2 * This document is a part of the source code and related artifacts
3 * for CollectionSpace, an open source collections management system
4 * for museums and related institutions:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright 2009 University of California at Berkeley
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
16 * https://source.collectionspace.org/collection-space/LICENSE.txt
18 * Unless required by applicable law or agreed to in writing, software
19 * distributed under the License is distributed on an "AS IS" BASIS,
20 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 * See the License for the specific language governing permissions and
22 * limitations under the License.
24 * This document is a part of the source code and related artifacts
25 * for CollectionSpace, an open source collections management system
26 * for museums and related institutions:
28 * http://www.collectionspace.org
29 * http://wiki.collectionspace.org
31 * Copyright 2009 University of California at Berkeley
33 * Licensed under the Educational Community License (ECL), Version 2.0.
34 * You may not use this file except in compliance with this License.
36 * You may obtain a copy of the ECL 2.0 License at
38 * https://source.collectionspace.org/collection-space/LICENSE.txt
40 * Unless required by applicable law or agreed to in writing, software
41 * distributed under the License is distributed on an "AS IS" BASIS,
42 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
43 * See the License for the specific language governing permissions and
44 * limitations under the License.
47 * To change this template, choose Tools | Templates
48 * and open the template in the editor.
50 package org.collectionspace.authentication.realm.db;
52 import java.lang.reflect.Constructor;
53 import java.net.ConnectException;
54 import java.security.Principal;
55 import java.security.acl.Group;
56 import java.sql.Connection;
57 import java.sql.PreparedStatement;
58 import java.sql.ResultSet;
59 import java.sql.SQLException;
60 import java.util.Collection;
61 import java.util.HashMap;
64 import javax.naming.Context;
65 import javax.naming.InitialContext;
66 import javax.naming.NamingException;
67 import javax.security.auth.login.FailedLoginException;
68 import javax.security.auth.login.LoginException;
69 import javax.sql.DataSource;
71 //import org.apache.commons.logging.Log;
72 //import org.apache.commons.logging.LogFactory;
78 import org.collectionspace.authentication.AuthN;
79 import org.collectionspace.authentication.CSpaceTenant;
80 import org.collectionspace.authentication.realm.CSpaceRealm;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
85 * CSpaceDbRealm provides access to user, password, role, tenant database
88 public class CSpaceDbRealm implements CSpaceRealm {
89 private Logger logger = LoggerFactory.getLogger(CSpaceDbRealm.class);
91 private String datasourceName;
92 private String principalsQuery;
93 private String rolesQuery;
94 private String tenantsQueryNoDisabled;
95 private String tenantsQueryWithDisabled;
96 private boolean suspendResume;
98 private long maxRetrySeconds = MAX_RETRY_SECONDS;
99 private static final int MAX_RETRY_SECONDS = 5;
100 private static final String MAX_RETRY_SECONDS_STR = "maxRetrySeconds";
102 private long delayBetweenAttemptsMillis = DELAY_BETWEEN_ATTEMPTS_MILLISECONDS;
103 private static final String DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR = "delayBetweenAttemptsMillis";
104 private static final long DELAY_BETWEEN_ATTEMPTS_MILLISECONDS = 200;
106 protected void setMaxRetrySeconds(Map options) {
107 Object optionsObj = options.get(MAX_RETRY_SECONDS_STR);
108 if (optionsObj != null) {
109 String paramValue = optionsObj.toString();
111 maxRetrySeconds = Long.parseLong(paramValue);
112 } catch (NumberFormatException e) {
113 logger.warn(String.format("The Spring Security login authentication parameter '%s' with value '%s' could not be parsed to a long value. The default value of '%d' will be used instead.",
114 MAX_RETRY_SECONDS_STR, paramValue, maxRetrySeconds));
119 protected long getMaxRetrySeconds() {
120 return this.maxRetrySeconds;
123 protected void setDelayBetweenAttemptsMillis(Map options) {
124 Object optionsObj = options.get(DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR);
125 if (optionsObj != null) {
126 String paramValue = optionsObj.toString();
128 delayBetweenAttemptsMillis = Long.parseLong(paramValue);
129 } catch (NumberFormatException e) {
130 logger.warn(String.format("The Spring Security login authentication parameter '%s' with value '%s' could not be parsed to a long value. The default value of '%d' will be used instead.",
131 MAX_RETRY_SECONDS_STR, paramValue, delayBetweenAttemptsMillis));
136 protected long getDelayBetweenAttemptsMillis() {
137 return this.delayBetweenAttemptsMillis;
141 * CSpace Database Realm
142 * @param datasourceName datasource name
144 public CSpaceDbRealm(Map options) {
145 datasourceName = (String) options.get("dsJndiName");
146 if (datasourceName == null) {
147 datasourceName = "java:/DefaultDS";
149 Object tmp = options.get("principalsQuery");
151 principalsQuery = tmp.toString();
153 tmp = options.get("rolesQuery");
155 rolesQuery = tmp.toString();
157 tmp = options.get("tenantsQueryNoDisabled");
159 tenantsQueryNoDisabled = tmp.toString();
161 tmp = options.get("tenantsQueryWithDisabled");
163 tenantsQueryWithDisabled = tmp.toString();
165 tmp = options.get("suspendResume");
167 suspendResume = Boolean.valueOf(tmp.toString()).booleanValue();
170 this.setMaxRetrySeconds(options);
171 this.setDelayBetweenAttemptsMillis(options);
173 if (logger.isTraceEnabled()) {
174 logger.trace("DatabaseServerLoginModule, dsJndiName=" + datasourceName);
175 logger.trace("principalsQuery=" + principalsQuery);
176 logger.trace("rolesQuery=" + rolesQuery);
177 logger.trace("suspendResume=" + suspendResume);
183 public String getUsersPassword(String username) throws LoginException {
185 String password = null;
186 Connection conn = null;
187 PreparedStatement ps = null;
190 conn = getConnection();
192 if (logger.isDebugEnabled()) {
193 logger.debug("Executing query: " + principalsQuery + ", with username: " + username);
195 ps = conn.prepareStatement(principalsQuery);
196 ps.setString(1, username);
197 rs = ps.executeQuery();
198 if (rs.next() == false) {
199 if (logger.isDebugEnabled()) {
200 logger.debug(principalsQuery + " returned no matches from db");
202 throw new FailedLoginException("No matching username found");
205 password = rs.getString(1);
206 } catch (SQLException ex) {
207 if (logger.isTraceEnabled() == true) {
208 logger.error("Could not open database to read AuthN tables.", ex);
210 LoginException le = new LoginException("Authentication query failed: " + ex.getLocalizedMessage());
213 } catch (Exception ex) {
214 LoginException le = new LoginException("Unknown Exception");
221 } catch (SQLException e) {
227 } catch (SQLException e) {
233 } catch (SQLException ex) {
241 * Execute the rolesQuery against the datasourceName to obtain the roles for
242 * the authenticated user.
243 * @return collection containing the roles
246 public Collection<Group> getRoles(String username, String principalClassName, String groupClassName) throws LoginException {
248 if (logger.isDebugEnabled()) {
249 logger.debug("getRoleSets using rolesQuery: " + rolesQuery + ", username: " + username);
252 Connection conn = null;
253 HashMap<String, Group> groupsMap = new HashMap<String, Group>();
254 PreparedStatement ps = null;
258 conn = getConnection();
259 // Get the user role names
260 if (logger.isDebugEnabled()) {
261 logger.debug("Executing query: " + rolesQuery + ", with username: " + username);
264 ps = conn.prepareStatement(rolesQuery);
266 ps.setString(1, username);
267 } catch (ArrayIndexOutOfBoundsException ignore) {
268 // The query may not have any parameters so just try it
270 rs = ps.executeQuery();
271 if (rs.next() == false) {
272 if (logger.isDebugEnabled()) {
273 logger.debug("No roles found");
275 // if(aslm.getUnauthenticatedIdentity() == null){
276 // throw new FailedLoginException("No matching username found in Roles");
278 /* We are running with an unauthenticatedIdentity so create an
279 empty Roles set and return.
282 Group g = createGroup(groupClassName, "Roles");
283 groupsMap.put(g.getName(), g);
284 return groupsMap.values();
288 String roleName = rs.getString(1);
289 String groupName = rs.getString(2);
290 if (groupName == null || groupName.length() == 0) {
294 Group group = (Group) groupsMap.get(groupName);
296 group = createGroup(groupClassName, groupName);
297 groupsMap.put(groupName, group);
301 Principal p = createPrincipal(principalClassName, roleName);
302 if (logger.isDebugEnabled()) {
303 logger.debug("Assign user to role " + roleName);
307 } catch (Exception e) {
308 logger.error("Failed to create principal: " + roleName + " " + e.toString());
312 } catch (SQLException ex) {
313 LoginException le = new LoginException("Query failed");
316 } catch (Exception e) {
317 LoginException le = new LoginException("unknown exception");
324 } catch (SQLException e) {
330 } catch (SQLException e) {
336 } catch (Exception ex) {
342 return groupsMap.values();
346 public Collection<Group> getTenants(String username, String groupClassName) throws LoginException {
347 return getTenants(username, groupClassName, false);
350 private boolean userIsTenantManager(Connection conn, String username) {
351 String acctQuery = "SELECT csid FROM accounts_common WHERE userid=?";
352 PreparedStatement ps = null;
354 boolean accountIsTenantManager = false;
356 ps = conn.prepareStatement(acctQuery);
357 ps.setString(1, username);
358 rs = ps.executeQuery();
360 String acctCSID = rs.getString(1);
361 if(AuthN.TENANT_MANAGER_ACCT_ID.equals(acctCSID)) {
362 accountIsTenantManager = true;
365 } catch (SQLException ex) {
366 if(logger.isDebugEnabled()) {
367 logger.debug("userIsTenantManager query failed on SQL error: " + ex.getLocalizedMessage());
369 } catch (Exception e) {
370 if(logger.isDebugEnabled()) {
371 logger.debug("userIsTenantManager unknown error: " + e.getLocalizedMessage());
377 } catch (SQLException e) {
383 } catch (SQLException e) {
387 return accountIsTenantManager;
391 * Execute the tenantsQuery against the datasourceName to obtain the tenants for
392 * the authenticated user.
393 * @return collection containing the roles
396 public Collection<Group> getTenants(String username, String groupClassName, boolean includeDisabledTenants) throws LoginException {
398 String tenantsQuery = getTenantQuery(includeDisabledTenants);
400 if (logger.isDebugEnabled()) {
401 logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username);
404 Connection conn = null;
405 HashMap<String, Group> groupsMap = new HashMap<String, Group>();
406 PreparedStatement ps = null;
408 final String defaultGroupName = "Tenants";
411 conn = getConnection();
413 ps = conn.prepareStatement(tenantsQuery);
415 ps.setString(1, username);
416 } catch (ArrayIndexOutOfBoundsException ignore) {
417 // The query may not have any parameters so just try it
419 rs = ps.executeQuery();
420 if (rs.next() == false) {
421 Group group = (Group) groupsMap.get(defaultGroupName);
423 group = createGroup(groupClassName, defaultGroupName);
424 groupsMap.put(defaultGroupName, group);
426 // Check for the tenantManager
427 if(userIsTenantManager(conn, username)) {
428 if (logger.isDebugEnabled()) {
429 logger.debug("GetTenants called with tenantManager - synthesizing the pseudo-tenant");
432 Principal p = createTenant("PseudoTenant", AuthN.TENANT_MANAGER_ACCT_ID);
433 if (logger.isDebugEnabled()) {
434 logger.debug("Assign tenantManager to tenant " + AuthN.TENANT_MANAGER_ACCT_ID);
437 } catch (Exception e) {
438 logger.error("Failed to create pseudo-tenant: " + e.toString());
441 if (logger.isDebugEnabled()) {
442 logger.debug("No tenants found");
444 // We are running with an unauthenticatedIdentity so return an
445 // empty Tenants set.
446 // FIXME should this be allowed?
448 return groupsMap.values();
452 String tenantId = rs.getString(1);
453 String tenantName = rs.getString(2);
454 String groupName = rs.getString(3);
455 if (groupName == null || groupName.length() == 0) {
456 groupName = defaultGroupName;
459 Group group = (Group) groupsMap.get(groupName);
461 group = createGroup(groupClassName, groupName);
462 groupsMap.put(groupName, group);
466 Principal p = createTenant(tenantName, tenantId);
467 if (logger.isDebugEnabled()) {
468 logger.debug("Assign user to tenant " + tenantName);
472 } catch (Exception e) {
473 logger.error("Failed to create tenant: " + tenantName + " " + e.toString());
476 } catch (SQLException ex) {
477 LoginException le = new LoginException("Query failed");
480 } catch (Exception e) {
481 LoginException le = new LoginException("unknown exception");
488 } catch (SQLException e) {
494 } catch (SQLException e) {
500 } catch (Exception ex) {
506 return groupsMap.values();
509 private CSpaceTenant createTenant(String name, String id) throws Exception {
510 return new CSpaceTenant(name, id);
513 private Group createGroup(String groupClassName, String name) throws Exception {
514 return (Group) createPrincipal(groupClassName, name);
517 private Principal createPrincipal(String principalClassName, String name) throws Exception {
518 ClassLoader loader = Thread.currentThread().getContextClassLoader();
519 Class clazz = loader.loadClass(principalClassName);
520 Class[] ctorSig = {String.class};
521 Constructor ctor = clazz.getConstructor(ctorSig);
522 Object[] ctorArgs = {name};
523 Principal p = (Principal) ctor.newInstance(ctorArgs);
528 * This method will attempt to get a connection. If a network error prevents it from getting a connection on the first try
529 * it will retry for the next 'getMaxRetrySeconds()' seconds. If it is unable to get the connection then it will timeout and
530 * throw an exception.
532 private Connection getConnection() throws Exception {
533 Connection result = null;
534 boolean failed = true;
535 Exception lastException = null;
536 int requestAttempts = 0;
538 long quittingTime = System.currentTimeMillis() + getMaxRetrySeconds() * 1000; // This is how long we attempt retries
540 if (requestAttempts > 0) {
541 Thread.sleep(getDelayBetweenAttemptsMillis()); // Wait a little time between reattempts.
545 // proceed to the original request by calling doFilter()
546 result = this.getConnection(getDataSourceName());
547 if (result != null) {
549 break; // the request was successfully executed, so we can break out of this retry loop
552 throw new ConnectException(); // The 'response' argument indicated a network related failure, so let's throw a generic connection exception
554 } catch (Exception e) {
556 if (exceptionChainContainsNetworkError(lastException) == false) {
557 // Break if the exception chain does not contain a
558 // network related exception because we don't want to retry if it's not a network related failure
561 requestAttempts++; // keep track of how many times we've tried the request
563 } while (System.currentTimeMillis() < quittingTime); // keep trying until we run out of time
566 // Add a warning to the logs if we encountered *any* failures on our re-attempts. Only add the warning
567 // if we were eventually successful.
569 if (requestAttempts > 0 && failed == false) {
570 logger.warn(String.format("Request to get a connection from data source '%s' failed with exception '%s' at attempt number '%d' before finally succeeding on the next attempt.",
572 lastException.getClass().getName(),
576 if (failed == true) {
577 // If we get here, it means all of our attempts to get a successful call to chain.doFilter() have failed.
585 * Don't call this method directly. Instead, use the getConnection() method that take no arguments.
587 private Connection getConnection(String dataSourceName) throws LoginException, SQLException {
588 InitialContext ctx = null;
589 Connection conn = null;
590 DataSource ds = null;
593 ctx = new InitialContext();
595 ds = (DataSource) ctx.lookup(dataSourceName);
596 } catch (Exception e) {}
599 Context envCtx = (Context) ctx.lookup("java:comp/env");
600 ds = (DataSource) envCtx.lookup(dataSourceName);
601 } catch (Exception e) {}
604 Context envCtx = (Context) ctx.lookup("java:comp");
605 ds = (DataSource) envCtx.lookup(dataSourceName);
606 } catch (Exception e) {}
609 Context envCtx = (Context) ctx.lookup("java:");
610 ds = (DataSource) envCtx.lookup(dataSourceName);
611 } catch (Exception e) {}
614 Context envCtx = (Context) ctx.lookup("java");
615 ds = (DataSource) envCtx.lookup(dataSourceName);
616 } catch (Exception e) {}
619 ds = (DataSource) ctx.lookup("java:/" + dataSourceName);
620 } catch (Exception e) {}
623 ds = AuthN.getDataSource();
627 throw new IllegalArgumentException("datasource not found: " + dataSourceName);
630 conn = ds.getConnection();
632 conn = AuthN.getDataSource().getConnection(); //FIXME:REM - This is the result of some type of JNDI mess. Should try to solve this problem and clean up this code.
637 } catch (NamingException ex) {
638 LoginException le = new LoginException("Error looking up DataSource from: " + dataSourceName);
645 } catch (Exception e) {
646 e.printStackTrace(); // We should be using a logger here instead.
654 * @return the datasourceName
656 public String getDataSourceName() {
657 return datasourceName;
661 * @return the principalQuery
663 public String getPrincipalQuery() {
664 return principalsQuery;
668 * @param principalQuery the principalQuery to set
670 public void setPrincipalQuery(String principalQuery) {
671 this.principalsQuery = principalQuery;
675 * @return the roleQuery
677 public String getRoleQuery() {
682 * @param roleQuery the roleQuery to set
684 public void setRoleQuery(String roleQuery) {
685 this.rolesQuery = roleQuery;
689 * @return the tenantQuery
691 public String getTenantQuery(boolean includeDisabledTenants) {
692 return includeDisabledTenants?tenantsQueryWithDisabled:tenantsQueryNoDisabled;
696 * @param tenantQuery the tenantQuery to set
697 public void setTenantQuery(String tenantQuery) {
698 this.tenantsQueryNoDisabled = tenantQuery;
703 * This method crawls the exception chain looking for network related exceptions and
704 * returns 'true' if it finds one.
706 public static boolean exceptionChainContainsNetworkError(Throwable exceptionChain) {
707 boolean result = false;
708 Throwable cause = exceptionChain;
710 while (cause != null) {
711 if (isExceptionNetworkRelated(cause) == true) {
716 cause = cause.getCause();
723 * Return 'true' if the exception is in the "java.net" package.
725 private static boolean isExceptionNetworkRelated(Throwable cause) {
726 boolean result = false;
728 String className = cause.getClass().getCanonicalName();
729 if (className.contains("java.net") == true) {