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.net.ConnectException;
53 import java.sql.Connection;
54 import java.sql.PreparedStatement;
55 import java.sql.ResultSet;
56 import java.sql.SQLException;
57 import java.util.LinkedHashSet;
61 import javax.naming.Context;
62 import javax.naming.InitialContext;
63 import javax.naming.NamingException;
64 import javax.security.auth.login.AccountException;
65 import javax.security.auth.login.AccountNotFoundException;
66 import javax.sql.DataSource;
68 import org.collectionspace.authentication.AuthN;
69 import org.collectionspace.authentication.CSpaceTenant;
70 import org.collectionspace.authentication.realm.CSpaceRealm;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
75 * CSpaceDbRealm provides access to user, password, role, tenant database
78 public class CSpaceDbRealm implements CSpaceRealm {
79 private Logger logger = LoggerFactory.getLogger(CSpaceDbRealm.class);
81 private String datasourceName;
82 private String principalsQuery;
83 private String rolesQuery;
84 private String tenantsQueryNoDisabled;
85 private String tenantsQueryWithDisabled;
86 private boolean suspendResume;
88 private long maxRetrySeconds = MAX_RETRY_SECONDS;
89 private static final int MAX_RETRY_SECONDS = 5;
90 private static final String MAX_RETRY_SECONDS_STR = "maxRetrySeconds";
92 private long delayBetweenAttemptsMillis = DELAY_BETWEEN_ATTEMPTS_MILLISECONDS;
93 private static final String DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR = "delayBetweenAttemptsMillis";
94 private static final long DELAY_BETWEEN_ATTEMPTS_MILLISECONDS = 200;
96 protected void setMaxRetrySeconds(Map<String, ?> options) {
97 Object optionsObj = options.get(MAX_RETRY_SECONDS_STR);
98 if (optionsObj != null) {
99 String paramValue = optionsObj.toString();
101 maxRetrySeconds = Long.parseLong(paramValue);
102 } catch (NumberFormatException e) {
103 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.",
104 MAX_RETRY_SECONDS_STR, paramValue, maxRetrySeconds));
109 protected long getMaxRetrySeconds() {
110 return this.maxRetrySeconds;
113 protected void setDelayBetweenAttemptsMillis(Map<String, ?> options) {
114 Object optionsObj = options.get(DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR);
115 if (optionsObj != null) {
116 String paramValue = optionsObj.toString();
118 delayBetweenAttemptsMillis = Long.parseLong(paramValue);
119 } catch (NumberFormatException e) {
120 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.",
121 MAX_RETRY_SECONDS_STR, paramValue, delayBetweenAttemptsMillis));
126 protected long getDelayBetweenAttemptsMillis() {
127 return this.delayBetweenAttemptsMillis;
131 * CSpace Database Realm
132 * @param datasourceName datasource name
134 public CSpaceDbRealm(Map<String, ?> options) {
135 datasourceName = (String) options.get("dsJndiName");
136 if (datasourceName == null) {
137 datasourceName = "java:/DefaultDS";
139 Object tmp = options.get("principalsQuery");
141 principalsQuery = tmp.toString();
143 tmp = options.get("rolesQuery");
145 rolesQuery = tmp.toString();
147 tmp = options.get("tenantsQueryNoDisabled");
149 tenantsQueryNoDisabled = tmp.toString();
151 tmp = options.get("tenantsQueryWithDisabled");
153 tenantsQueryWithDisabled = tmp.toString();
155 tmp = options.get("suspendResume");
157 suspendResume = Boolean.valueOf(tmp.toString()).booleanValue();
160 this.setMaxRetrySeconds(options);
161 this.setDelayBetweenAttemptsMillis(options);
163 if (logger.isTraceEnabled()) {
164 logger.trace("DatabaseServerLoginModule, dsJndiName=" + datasourceName);
165 logger.trace("principalsQuery=" + principalsQuery);
166 logger.trace("rolesQuery=" + rolesQuery);
167 logger.trace("suspendResume=" + suspendResume);
173 public String getPassword(String username) throws AccountException {
175 String password = null;
176 Connection conn = null;
177 PreparedStatement ps = null;
180 conn = getConnection();
182 if (logger.isDebugEnabled()) {
183 logger.debug("Executing query: " + principalsQuery + ", with username: " + username);
185 ps = conn.prepareStatement(principalsQuery);
186 ps.setString(1, username);
187 rs = ps.executeQuery();
188 if (rs.next() == false) {
189 if (logger.isDebugEnabled()) {
190 logger.debug(principalsQuery + " returned no matches from db");
192 throw new AccountNotFoundException("No matching username found");
195 password = rs.getString(1);
196 } catch (SQLException ex) {
197 if (logger.isTraceEnabled() == true) {
198 logger.error("Could not open database to read AuthN tables.", ex);
200 AccountException ae = new AccountException("Authentication query failed: " + ex.getLocalizedMessage());
203 } catch (AccountNotFoundException ex) {
205 } catch (Exception ex) {
206 AccountException ae = new AccountException("Unknown Exception");
213 } catch (SQLException e) {
219 } catch (SQLException e) {
225 } catch (SQLException ex) {
233 public Set<String> getRoles(String username) throws AccountException {
234 if (logger.isDebugEnabled()) {
235 logger.debug("getRoleSets using rolesQuery: " + rolesQuery + ", username: " + username);
238 Set<String> roles = new LinkedHashSet<String>();
240 Connection conn = null;
241 PreparedStatement ps = null;
245 conn = getConnection();
246 // Get the user role names
247 if (logger.isDebugEnabled()) {
248 logger.debug("Executing query: " + rolesQuery + ", with username: " + username);
251 ps = conn.prepareStatement(rolesQuery);
253 ps.setString(1, username);
254 } catch (ArrayIndexOutOfBoundsException ignore) {
255 // The query may not have any parameters so just try it
257 rs = ps.executeQuery();
258 if (rs.next() == false) {
259 if (logger.isDebugEnabled()) {
260 logger.debug("No roles found");
267 String roleName = rs.getString(1);
271 } catch (SQLException ex) {
272 AccountException ae = new AccountException("Query failed");
275 } catch (Exception e) {
276 AccountException ae = new AccountException("unknown exception");
283 } catch (SQLException e) {
289 } catch (SQLException e) {
295 } catch (Exception ex) {
305 public Set<CSpaceTenant> getTenants(String username) throws AccountException {
306 return getTenants(username, false);
309 private boolean userIsTenantManager(Connection conn, String username) {
310 String acctQuery = "SELECT csid FROM accounts_common WHERE userid=?";
311 PreparedStatement ps = null;
313 boolean accountIsTenantManager = false;
315 ps = conn.prepareStatement(acctQuery);
316 ps.setString(1, username);
317 rs = ps.executeQuery();
319 String acctCSID = rs.getString(1);
320 if(AuthN.TENANT_MANAGER_ACCT_ID.equals(acctCSID)) {
321 accountIsTenantManager = true;
324 } catch (SQLException ex) {
325 if(logger.isDebugEnabled()) {
326 logger.debug("userIsTenantManager query failed on SQL error: " + ex.getLocalizedMessage());
328 } catch (Exception e) {
329 if(logger.isDebugEnabled()) {
330 logger.debug("userIsTenantManager unknown error: " + e.getLocalizedMessage());
336 } catch (SQLException e) {
342 } catch (SQLException e) {
346 return accountIsTenantManager;
350 * Execute the tenantsQuery against the datasourceName to obtain the tenants for
351 * the authenticated user.
352 * @return set containing the roles
355 public Set<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException {
357 String tenantsQuery = getTenantQuery(includeDisabledTenants);
359 if (logger.isDebugEnabled()) {
360 logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username);
363 Set<CSpaceTenant> tenants = new LinkedHashSet<CSpaceTenant>();
365 Connection conn = null;
366 PreparedStatement ps = null;
370 conn = getConnection();
372 ps = conn.prepareStatement(tenantsQuery);
374 ps.setString(1, username);
375 } catch (ArrayIndexOutOfBoundsException ignore) {
376 // The query may not have any parameters so just try it
378 rs = ps.executeQuery();
379 if (rs.next() == false) {
380 // Check for the tenantManager
381 if(userIsTenantManager(conn, username)) {
382 if (logger.isDebugEnabled()) {
383 logger.debug("GetTenants called with tenantManager - synthesizing the pseudo-tenant");
386 tenants.add(new CSpaceTenant(AuthN.TENANT_MANAGER_ACCT_ID, "PseudoTenant"));
388 if (logger.isDebugEnabled()) {
389 logger.debug("No tenants found");
391 // We are running with an unauthenticatedIdentity so return an
392 // empty Tenants set.
393 // FIXME should this be allowed?
400 String tenantId = rs.getString(1);
401 String tenantName = rs.getString(2);
403 tenants.add(new CSpaceTenant(tenantId, tenantName));
405 } catch (SQLException ex) {
406 AccountException ae = new AccountException("Query failed");
409 } catch (Exception e) {
410 AccountException ae = new AccountException("unknown exception");
417 } catch (SQLException e) {
423 } catch (SQLException e) {
429 } catch (Exception ex) {
438 * This method will attempt to get a connection. If a network error prevents it from getting a connection on the first try
439 * it will retry for the next 'getMaxRetrySeconds()' seconds. If it is unable to get the connection then it will timeout and
440 * throw an exception.
442 private Connection getConnection() throws Exception {
443 Connection result = null;
444 boolean failed = true;
445 Exception lastException = null;
446 int requestAttempts = 0;
448 long quittingTime = System.currentTimeMillis() + getMaxRetrySeconds() * 1000; // This is how long we attempt retries
450 if (requestAttempts > 0) {
451 Thread.sleep(getDelayBetweenAttemptsMillis()); // Wait a little time between reattempts.
455 // proceed to the original request by calling doFilter()
456 result = this.getConnection(getDataSourceName());
457 if (result != null) {
459 break; // the request was successfully executed, so we can break out of this retry loop
462 throw new ConnectException(); // The 'response' argument indicated a network related failure, so let's throw a generic connection exception
464 } catch (Exception e) {
466 if (exceptionChainContainsNetworkError(lastException) == false) {
467 // Break if the exception chain does not contain a
468 // network related exception because we don't want to retry if it's not a network related failure
471 requestAttempts++; // keep track of how many times we've tried the request
473 } while (System.currentTimeMillis() < quittingTime); // keep trying until we run out of time
476 // Add a warning to the logs if we encountered *any* failures on our re-attempts. Only add the warning
477 // if we were eventually successful.
479 if (requestAttempts > 0 && failed == false) {
480 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.",
482 lastException.getClass().getName(),
486 if (failed == true) {
487 // If we get here, it means all of our attempts to get a successful call to chain.doFilter() have failed.
495 * Don't call this method directly. Instead, use the getConnection() method that take no arguments.
497 private Connection getConnection(String dataSourceName) throws AccountException, SQLException {
498 InitialContext ctx = null;
499 Connection conn = null;
500 DataSource ds = null;
503 ctx = new InitialContext();
505 ds = (DataSource) ctx.lookup(dataSourceName);
506 } catch (Exception e) {}
509 Context envCtx = (Context) ctx.lookup("java:comp/env");
510 ds = (DataSource) envCtx.lookup(dataSourceName);
511 } catch (Exception e) {}
514 Context envCtx = (Context) ctx.lookup("java:comp");
515 ds = (DataSource) envCtx.lookup(dataSourceName);
516 } catch (Exception e) {}
519 Context envCtx = (Context) ctx.lookup("java:");
520 ds = (DataSource) envCtx.lookup(dataSourceName);
521 } catch (Exception e) {}
524 Context envCtx = (Context) ctx.lookup("java");
525 ds = (DataSource) envCtx.lookup(dataSourceName);
526 } catch (Exception e) {}
529 ds = (DataSource) ctx.lookup("java:/" + dataSourceName);
530 } catch (Exception e) {}
533 ds = AuthN.getDataSource();
537 throw new IllegalArgumentException("datasource not found: " + dataSourceName);
540 conn = ds.getConnection();
542 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.
547 } catch (NamingException ex) {
548 AccountException ae = new AccountException("Error looking up DataSource from: " + dataSourceName);
555 } catch (Exception e) {
556 e.printStackTrace(); // We should be using a logger here instead.
564 * @return the datasourceName
566 public String getDataSourceName() {
567 return datasourceName;
571 * @return the principalQuery
573 public String getPrincipalQuery() {
574 return principalsQuery;
578 * @param principalQuery the principalQuery to set
580 public void setPrincipalQuery(String principalQuery) {
581 this.principalsQuery = principalQuery;
585 * @return the roleQuery
587 public String getRoleQuery() {
592 * @param roleQuery the roleQuery to set
594 public void setRoleQuery(String roleQuery) {
595 this.rolesQuery = roleQuery;
599 * @return the tenantQuery
601 public String getTenantQuery(boolean includeDisabledTenants) {
602 return includeDisabledTenants?tenantsQueryWithDisabled:tenantsQueryNoDisabled;
606 * @param tenantQuery the tenantQuery to set
607 public void setTenantQuery(String tenantQuery) {
608 this.tenantsQueryNoDisabled = tenantQuery;
613 * This method crawls the exception chain looking for network related exceptions and
614 * returns 'true' if it finds one.
616 public static boolean exceptionChainContainsNetworkError(Throwable exceptionChain) {
617 boolean result = false;
618 Throwable cause = exceptionChain;
620 while (cause != null) {
621 if (isExceptionNetworkRelated(cause) == true) {
626 cause = cause.getCause();
633 * Return 'true' if the exception is in the "java.net" package.
635 private static boolean isExceptionNetworkRelated(Throwable cause) {
636 boolean result = false;
638 String className = cause.getClass().getCanonicalName();
639 if (className.contains("java.net") == true) {