Merge lp:~nwilliams/akiban-server/security-service-login-service into lp:~akiban-technologies/akiban-server/trunk

Proposed by Nathan Williams
Status: Merged
Approved by: Thomas Jones-Low
Approved revision: 2638
Merged at revision: 2634
Proposed branch: lp:~nwilliams/akiban-server/security-service-login-service
Merge into: lp:~akiban-technologies/akiban-server/trunk
Diff against target: 540 lines (+256/-118)
9 files modified
src/main/java/com/akiban/http/HttpConductorImpl.java (+8/-47)
src/main/java/com/akiban/http/SecurityServiceLoginService.java (+72/-0)
src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java (+6/-4)
src/main/java/com/akiban/server/service/security/User.java (+17/-1)
src/main/resources/com/akiban/http/basic.properties (+0/-33)
src/main/resources/com/akiban/http/digest.properties (+0/-33)
src/main/resources/com/akiban/server/service/config/configuration-defaults.properties (+3/-0)
src/test/java/com/akiban/http/HttpThreadedLoginIT.java (+147/-0)
src/test/java/com/akiban/server/service/security/SecurityServiceIT.java (+3/-0)
To merge this branch: bzr merge lp:~nwilliams/akiban-server/security-service-login-service
Reviewer Review Type Date Requested Status
Thomas Jones-Low Approve
Review via email: mp+160229@code.launchpad.net

Description of the change

Replace SingleThreadedJDBCService with new one backed directly by the SecurityService.

The JDBCService is racey and doesn't work with the requirements of our EmbeddedJDBCService so just ditch it.

Expose more of the information off of the User (password hashes, all roles). It wasn't a direct concern, but the new LoginService also has a cache time so the performance should be identical.

The test failed quite consistently for the 5, 10, and 20 thread cases before the fix. Now passes as desired.

To post a comment you must log in.
2638. By Nathan Williams

Remove unneeded import

Revision history for this message
Thomas Jones-Low (tjoneslo) wrote :

Well, this will certainly make user process tracking simpler.
Otherwise looks as described.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/main/java/com/akiban/http/HttpConductorImpl.java'
2--- src/main/java/com/akiban/http/HttpConductorImpl.java 2013-04-14 16:11:04 +0000
3+++ src/main/java/com/akiban/http/HttpConductorImpl.java 2013-04-22 23:01:29 +0000
4@@ -17,19 +17,18 @@
5
6 package com.akiban.http;
7
8-import com.akiban.server.error.AkibanInternalException;
9 import com.akiban.server.service.Service;
10 import com.akiban.server.service.config.ConfigurationService;
11 import com.akiban.server.service.monitor.MonitorService;
12 import com.akiban.server.service.monitor.ServerMonitor;
13 import com.akiban.server.service.security.SecurityService;
14+import com.akiban.sql.embedded.EmbeddedJDBCService;
15 import com.google.inject.Inject;
16 import org.eclipse.jetty.servlets.CrossOriginFilter;
17 import org.eclipse.jetty.util.security.Constraint;
18 import org.eclipse.jetty.security.Authenticator;
19 import org.eclipse.jetty.security.ConstraintMapping;
20 import org.eclipse.jetty.security.ConstraintSecurityHandler;
21-import org.eclipse.jetty.security.JDBCLoginService;
22 import org.eclipse.jetty.security.LoginService;
23 import org.eclipse.jetty.security.authentication.BasicAuthenticator;
24 import org.eclipse.jetty.security.authentication.DigestAuthenticator;
25@@ -45,8 +44,6 @@
26
27 import javax.servlet.FilterRegistration;
28 import javax.servlet.ServletException;
29-import java.io.IOException;
30-import java.lang.reflect.Method;
31 import java.util.Collections;
32 import java.util.HashSet;
33 import java.util.Set;
34@@ -59,6 +56,7 @@
35 private static final String CONFIG_PORT_PROPERTY = CONFIG_HTTP_PREFIX + "port";
36 private static final String CONFIG_SSL_PROPERTY = CONFIG_HTTP_PREFIX + "ssl";
37 private static final String CONFIG_LOGIN_PROPERTY = CONFIG_HTTP_PREFIX + "login";
38+ private static final String CONFIG_LOGIN_CACHE_SECONDS = CONFIG_HTTP_PREFIX + "login_cache_seconds";
39 private static final String CONFIG_XORIGIN_PREFIX = CONFIG_HTTP_PREFIX + "cross_origin.";
40 private static final String CONFIG_XORIGIN_ENABLED = CONFIG_XORIGIN_PREFIX + "enabled";
41 private static final String CONFIG_XORIGIN_ORIGINS = CONFIG_XORIGIN_PREFIX + "allowed_origins";
42@@ -152,6 +150,7 @@
43 String portProperty = configurationService.getProperty(CONFIG_PORT_PROPERTY);
44 String sslProperty = configurationService.getProperty(CONFIG_SSL_PROPERTY);
45 String loginProperty = configurationService.getProperty(CONFIG_LOGIN_PROPERTY);
46+ int loginCacheSeconds = Integer.parseInt(configurationService.getProperty(CONFIG_LOGIN_CACHE_SECONDS));
47 int portLocal;
48 boolean ssl;
49 AuthenticationType login;
50@@ -210,20 +209,20 @@
51 localServer.setHandler(localHandlerList);
52 }
53 else {
54- String resource;
55+ SecurityServiceLoginService.CredentialType credentialType;
56 Authenticator authenticator;
57 switch (login) {
58 case BASIC:
59- resource = "basic.properties";
60+ credentialType = SecurityServiceLoginService.CredentialType.BASIC;
61 authenticator = new BasicAuthenticator();
62 break;
63 case DIGEST:
64- resource = "digest.properties";
65+ credentialType = SecurityServiceLoginService.CredentialType.DIGEST;
66 authenticator = new DigestAuthenticator();
67 break;
68 default:
69 assert false : "Unexpected authentication type " + login;
70- resource = null;
71+ credentialType = null;
72 authenticator = null;
73 }
74 Constraint constraint = new Constraint(authenticator.getAuthMethod(),
75@@ -238,9 +237,7 @@
76 sh.setAuthenticator(authenticator);
77 sh.setConstraintMappings(Collections.singletonList(cm));
78
79- LoginService loginService =
80- new SingleThreadJDBCLoginService(SecurityService.REALM,
81- HttpConductorImpl.class.getResource(resource).toString());
82+ LoginService loginService = new SecurityServiceLoginService(securityService, credentialType, loginCacheSeconds);
83 sh.setLoginService(loginService);
84
85 sh.setHandler(localHandlerList);
86@@ -315,42 +312,6 @@
87 return result;
88 }
89
90- // Embedded JDBC is single-threaded, but login service assumes it
91- // is thread-safe. Also, it does not close its ResultSet, which
92- // leaves a transaction active. So just close connection around
93- // each use. Login service is already
94- // synchronized. Unfortunately, this needs a private method.
95- static class SingleThreadJDBCLoginService extends JDBCLoginService {
96- private Method closeMethod;
97-
98- public SingleThreadJDBCLoginService(String name, String config)
99- throws IOException {
100- super(name, config);
101- try {
102- closeMethod = JDBCLoginService.class.getDeclaredMethod("closeConnection", null);
103- closeMethod.setAccessible(true);
104- }
105- catch (Exception ex) {
106- throw new AkibanInternalException("Cannot get JDBC close method", ex);
107- }
108- }
109-
110- @Override
111- protected org.eclipse.jetty.server.UserIdentity loadUser(String username) {
112- try {
113- return super.loadUser(username);
114- }
115- finally {
116- try {
117- closeMethod.invoke(this);
118- }
119- catch (Exception ex) {
120- logger.warn("Cannot call JDBC close method", ex);
121- }
122- }
123- }
124- }
125-
126 private class ConnectionMonitor implements ServerMonitor {
127 public static final String SERVER_TYPE = "REST";
128 private final SelectChannelConnector connector;
129
130=== added file 'src/main/java/com/akiban/http/SecurityServiceLoginService.java'
131--- src/main/java/com/akiban/http/SecurityServiceLoginService.java 1970-01-01 00:00:00 +0000
132+++ src/main/java/com/akiban/http/SecurityServiceLoginService.java 2013-04-22 23:01:29 +0000
133@@ -0,0 +1,72 @@
134+/**
135+ * Copyright (C) 2009-2013 Akiban Technologies, Inc.
136+ *
137+ * This program is free software: you can redistribute it and/or modify
138+ * it under the terms of the GNU Affero General Public License as published by
139+ * the Free Software Foundation, either version 3 of the License, or
140+ * (at your option) any later version.
141+ *
142+ * This program is distributed in the hope that it will be useful,
143+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
144+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
145+ * GNU Affero General Public License for more details.
146+ *
147+ * You should have received a copy of the GNU Affero General Public License
148+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
149+ */
150+
151+package com.akiban.http;
152+
153+import java.util.List;
154+
155+import com.akiban.server.service.security.SecurityService;
156+import com.akiban.server.service.security.User;
157+import com.akiban.util.ArgumentValidation;
158+import org.eclipse.jetty.security.MappedLoginService;
159+import org.eclipse.jetty.server.UserIdentity;
160+import org.eclipse.jetty.util.security.Credential;
161+
162+public class SecurityServiceLoginService extends MappedLoginService
163+{
164+ public enum CredentialType { BASIC, DIGEST }
165+
166+ private final SecurityService securityService;
167+ private final CredentialType credentialType;
168+ private final long cacheMillis;
169+ private volatile long lastCachePurge;
170+
171+ public SecurityServiceLoginService(SecurityService securityService, CredentialType credentialType, int cacheSeconds) {
172+ ArgumentValidation.isGTE("cacheSeconds", cacheSeconds, 0);
173+ if(credentialType != CredentialType.BASIC && credentialType != CredentialType.DIGEST) {
174+ throw new IllegalArgumentException("Unknown credential: " + credentialType);
175+ }
176+ this.securityService = securityService;
177+ this.credentialType = credentialType;
178+ this.cacheMillis = cacheSeconds * 1000;
179+ }
180+
181+ @Override
182+ public UserIdentity login(String username, Object credentials) {
183+ long now = System.currentTimeMillis();
184+ if((now - lastCachePurge) > cacheMillis) {
185+ super._users.clear();
186+ lastCachePurge = now;
187+ }
188+ return super.login(username, credentials);
189+ }
190+
191+ @Override
192+ protected void loadUsers() {
193+ }
194+
195+ @Override
196+ protected UserIdentity loadUser(String username) {
197+ User user = securityService.getUser(username);
198+ if(user != null) {
199+ String password = (credentialType == CredentialType.BASIC) ? user.getBasicPassword() : user.getDigestPassword();
200+ List<String> roles = user.getRoles();
201+ return putUser(username, Credential.getCredential(password), roles.toArray(new String[roles.size()]));
202+ }
203+ return null;
204+ }
205+}
206
207=== modified file 'src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java'
208--- src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java 2013-03-27 10:45:17 +0000
209+++ src/main/java/com/akiban/server/service/security/SecurityServiceImpl.java 2013-04-22 23:01:29 +0000
210@@ -204,7 +204,7 @@
211 roles.add(rs1.getString(2));
212 }
213 rs1.close();
214- return new User(rs.getInt(1), rs.getString(2), roles);
215+ return new User(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), roles);
216 }
217
218 @Override
219@@ -212,12 +212,14 @@
220 int id;
221 Connection conn = null;
222 PreparedStatement stmt = null;
223+ String basicPassword = basicPassword(password);
224+ String digestPassword = digestPassword(name, password);
225 try {
226 conn = openConnection();
227 stmt = conn.prepareStatement(ADD_USER_SQL);
228 stmt.setString(1, name);
229- stmt.setString(2, basicPassword(password));
230- stmt.setString(3, digestPassword(name, password));
231+ stmt.setString(2, basicPassword);
232+ stmt.setString(3, digestPassword);
233 stmt.setString(4, md5Password(name, password));
234 int nrows = stmt.executeUpdate();
235 if (nrows != 1) {
236@@ -250,7 +252,7 @@
237 finally {
238 cleanup(conn, stmt);
239 }
240- return new User(id, name, new ArrayList<>(roles));
241+ return new User(id, name, basicPassword, digestPassword, new ArrayList<>(roles));
242 }
243
244 @Override
245
246=== modified file 'src/main/java/com/akiban/server/service/security/User.java'
247--- src/main/java/com/akiban/server/service/security/User.java 2013-03-22 20:05:57 +0000
248+++ src/main/java/com/akiban/server/service/security/User.java 2013-04-22 23:01:29 +0000
249@@ -25,11 +25,15 @@
250 {
251 private final int id;
252 private final String name;
253+ private final String basicPassword;
254+ private final String digestPassword;
255 private final List<String> roles;
256
257- protected User(int id, String name, List<String> roles) {
258+ protected User(int id, String name, String basicPassword, String digestPassword, List<String> roles) {
259 this.id = id;
260 this.name = name;
261+ this.basicPassword = basicPassword;
262+ this.digestPassword = digestPassword;
263 this.roles = roles;
264 }
265
266@@ -42,6 +46,18 @@
267 return name;
268 }
269
270+ public String getBasicPassword() {
271+ return basicPassword;
272+ }
273+
274+ public String getDigestPassword() {
275+ return digestPassword;
276+ }
277+
278+ public List<String> getRoles() {
279+ return roles;
280+ }
281+
282 public boolean hasRole(String role) {
283 return roles.contains(role);
284 }
285
286=== removed file 'src/main/resources/com/akiban/http/basic.properties'
287--- src/main/resources/com/akiban/http/basic.properties 2013-03-22 20:05:57 +0000
288+++ src/main/resources/com/akiban/http/basic.properties 1970-01-01 00:00:00 +0000
289@@ -1,33 +0,0 @@
290-#
291-# Copyright (C) 2009-2013 Akiban Technologies, Inc.
292-#
293-# This program is free software: you can redistribute it and/or modify
294-# it under the terms of the GNU Affero General Public License as published by
295-# the Free Software Foundation, either version 3 of the License, or
296-# (at your option) any later version.
297-#
298-# This program is distributed in the hope that it will be useful,
299-# but WITHOUT ANY WARRANTY; without even the implied warranty of
300-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
301-# GNU Affero General Public License for more details.
302-#
303-# You should have received a copy of the GNU Affero General Public License
304-# along with this program. If not, see <http://www.gnu.org/licenses/>.
305-#
306-
307-# Driver doesn't matter (already loaded), but must have no-arg ctor.
308-jdbcdriver=java.lang.Object
309-url=jdbc:default:connection
310-username=akiban
311-password=akiban
312-usertable=security_schema.users
313-usertablekey=id
314-usertableuserfield=name
315-usertablepasswordfield=password_basic
316-roletable=security_schema.roles
317-roletablekey=id
318-roletablerolefield=name
319-userroletable=security_schema.user_roles
320-userroletableuserkey=user_id
321-userroletablerolekey=role_id
322-cachetime=300
323
324=== removed file 'src/main/resources/com/akiban/http/digest.properties'
325--- src/main/resources/com/akiban/http/digest.properties 2013-03-22 20:05:57 +0000
326+++ src/main/resources/com/akiban/http/digest.properties 1970-01-01 00:00:00 +0000
327@@ -1,33 +0,0 @@
328-#
329-# Copyright (C) 2009-2013 Akiban Technologies, Inc.
330-#
331-# This program is free software: you can redistribute it and/or modify
332-# it under the terms of the GNU Affero General Public License as published by
333-# the Free Software Foundation, either version 3 of the License, or
334-# (at your option) any later version.
335-#
336-# This program is distributed in the hope that it will be useful,
337-# but WITHOUT ANY WARRANTY; without even the implied warranty of
338-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
339-# GNU Affero General Public License for more details.
340-#
341-# You should have received a copy of the GNU Affero General Public License
342-# along with this program. If not, see <http://www.gnu.org/licenses/>.
343-#
344-
345-# Driver doesn't matter (already loaded), but must have no-arg ctor.
346-jdbcdriver=java.lang.Object
347-url=jdbc:default:connection
348-username=akiban
349-password=akiban
350-usertable=security_schema.users
351-usertablekey=id
352-usertableuserfield=name
353-usertablepasswordfield=password_digest
354-roletable=security_schema.roles
355-roletablekey=id
356-roletablerolefield=name
357-userroletable=security_schema.user_roles
358-userroletableuserkey=user_id
359-userroletablerolekey=role_id
360-cachetime=300
361
362=== modified file 'src/main/resources/com/akiban/server/service/config/configuration-defaults.properties'
363--- src/main/resources/com/akiban/server/service/config/configuration-defaults.properties 2013-04-15 16:20:25 +0000
364+++ src/main/resources/com/akiban/server/service/config/configuration-defaults.properties 2013-04-22 23:01:29 +0000
365@@ -82,6 +82,9 @@
366 akserver.http.ssl=false
367 akserver.http.login=none
368
369+# How long to save credential look-ups
370+akserver.http.login_cache_seconds = 300
371+
372 # If enabled, use a CrossOriginFilter and construct it with these defaults.
373 akserver.http.cross_origin.enabled = true
374 akserver.http.cross_origin.allowed_origins = *
375
376=== added file 'src/test/java/com/akiban/http/HttpThreadedLoginIT.java'
377--- src/test/java/com/akiban/http/HttpThreadedLoginIT.java 1970-01-01 00:00:00 +0000
378+++ src/test/java/com/akiban/http/HttpThreadedLoginIT.java 2013-04-22 23:01:29 +0000
379@@ -0,0 +1,147 @@
380+/**
381+ * Copyright (C) 2009-2013 Akiban Technologies, Inc.
382+ *
383+ * This program is free software: you can redistribute it and/or modify
384+ * it under the terms of the GNU Affero General Public License as published by
385+ * the Free Software Foundation, either version 3 of the License, or
386+ * (at your option) any later version.
387+ *
388+ * This program is distributed in the hope that it will be useful,
389+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
390+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
391+ * GNU Affero General Public License for more details.
392+ *
393+ * You should have received a copy of the GNU Affero General Public License
394+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
395+ */
396+
397+package com.akiban.http;
398+
399+import com.akiban.rest.RestService;
400+import com.akiban.rest.RestServiceImpl;
401+import com.akiban.server.service.servicemanager.GuicedServiceManager;
402+import com.akiban.server.test.it.ITBase;
403+import com.akiban.sql.embedded.EmbeddedJDBCService;
404+import com.akiban.sql.embedded.EmbeddedJDBCServiceImpl;
405+
406+import org.apache.http.HttpResponse;
407+import org.apache.http.HttpStatus;
408+import org.apache.http.client.HttpClient;
409+import org.apache.http.client.methods.HttpGet;
410+import org.apache.http.impl.client.DefaultHttpClient;
411+import org.apache.http.util.EntityUtils;
412+import org.junit.Test;
413+import org.slf4j.Logger;
414+import org.slf4j.LoggerFactory;
415+
416+import static org.junit.Assert.*;
417+
418+import java.net.URI;
419+import java.util.Collections;
420+import java.util.HashMap;
421+import java.util.Map;
422+
423+public class HttpThreadedLoginIT extends ITBase
424+{
425+ private static final Logger LOG = LoggerFactory.getLogger(HttpThreadedLoginIT.class);
426+
427+ @Override
428+ protected GuicedServiceManager.BindingsConfigurationProvider serviceBindingsProvider() {
429+ return super.serviceBindingsProvider()
430+ .bindAndRequire(RestService.class, RestServiceImpl.class);
431+ }
432+
433+ @Override
434+ protected Map<String, String> startupConfigProperties() {
435+ Map<String, String> properties = new HashMap<>();
436+ properties.put("akserver.http.login", "basic");
437+ return properties;
438+ }
439+
440+ private static int openRestURL(String userInfo, int port, String path) throws Exception {
441+ HttpClient client = new DefaultHttpClient();
442+ URI uri = new URI("http", userInfo, "localhost", port, path, null, null);
443+ HttpGet get = new HttpGet(uri);
444+ HttpResponse response = client.execute(get);
445+ int code = response.getStatusLine().getStatusCode();
446+ EntityUtils.consume(response.getEntity());
447+ client.getConnectionManager().shutdown();
448+ return code;
449+ }
450+
451+ @Test
452+ public void oneThread() throws InterruptedException {
453+ run(1);
454+ }
455+
456+ @Test
457+ public void fiveThreads() throws InterruptedException {
458+ run(5);
459+ }
460+
461+ @Test
462+ public void tenThreads() throws InterruptedException {
463+ run(10);
464+ }
465+
466+ @Test
467+ public void twentyThreads() throws InterruptedException {
468+ run(20);
469+ }
470+
471+
472+ private void run(int count) throws InterruptedException {
473+ final int port = serviceManager().getServiceByClass(HttpConductor.class).getPort();
474+ final String context = serviceManager().getServiceByClass(RestService.class).getContextPath();
475+ final String path = context + "/version";
476+ final UncaughtHandler uncaughtHandler = new UncaughtHandler();
477+
478+ Thread threads[] = new Thread[count];
479+ for(int i = 0; i < count; ++i) {
480+ threads[i] = new Thread(new TestRunnable(port, path, i), "Thread"+i);
481+ threads[i].setUncaughtExceptionHandler(uncaughtHandler);
482+ }
483+ for(int i = 0; i < count; ++i) {
484+ threads[i].start();
485+ }
486+ for(int i = 0; i < count; ++i) {
487+ threads[i].join();
488+ }
489+
490+ for(Throwable entry : uncaughtHandler.uncaught.values()) {
491+ LOG.error("Uncaught exception", entry);
492+ }
493+ assertEquals("uncaught exception count", 0, uncaughtHandler.uncaught.size());
494+ }
495+
496+ private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
497+ public final Map<Thread,Throwable> uncaught = Collections.synchronizedMap(new HashMap<Thread,Throwable>());
498+
499+ @Override
500+ public void uncaughtException(Thread t, Throwable e) {
501+ uncaught.put(t, e);
502+ }
503+ }
504+
505+ private static class TestRunnable implements Runnable {
506+ private final int port;
507+ private final String url;
508+ private final int userNum;
509+
510+ public TestRunnable(int port, String url, int userNum) {
511+ this.port = port;
512+ this.url = url;
513+ this.userNum = userNum;
514+ }
515+
516+ @Override
517+ public void run() {
518+ String userInfo = String.format("user_%d:password", userNum);
519+ try {
520+ assertEquals(userInfo, HttpStatus.SC_UNAUTHORIZED, openRestURL(userInfo, port, url));
521+ } catch(Exception e) {
522+ throw new RuntimeException(e);
523+ }
524+ }
525+ }
526+}
527
528=== modified file 'src/test/java/com/akiban/server/service/security/SecurityServiceIT.java'
529--- src/test/java/com/akiban/server/service/security/SecurityServiceIT.java 2013-04-07 07:28:15 +0000
530+++ src/test/java/com/akiban/server/service/security/SecurityServiceIT.java 2013-04-22 23:01:29 +0000
531@@ -101,6 +101,9 @@
532 assertNotNull("user found", user);
533 assertTrue("user has role", user.hasRole("rest-user"));
534 assertFalse("user does not have role", user.hasRole("admin"));
535+ assertEquals("users roles", "[rest-user]", user.getRoles().toString());
536+ assertEquals("user password basic", "MD5:5F4DCC3B5AA765D61D8327DEB882CF99", user.getBasicPassword());
537+ assertEquals("user password digest", "MD5:740C53C6C7EE87EEEE489E8CE4116048", user.getDigestPassword());
538 }
539
540 @Test

Subscribers

People subscribed via source and target branches