]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
2b13bf7ebfffe1b2512179ea517d2a92f099ca33
[tmp/jakarta-migration.git] /
1 /**
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:
5
6  *  http://www.collectionspace.org
7  *  http://wiki.collectionspace.org
8
9  *  Copyright 2009 University of California at Berkeley
10
11  *  Licensed under the Educational Community License (ECL), Version 2.0.
12  *  You may not use this file except in compliance with this License.
13
14  *  You may obtain a copy of the ECL 2.0 License at
15
16  *  https://source.collectionspace.org/collection-space/LICENSE.txt
17
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.
23  *//**
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:
27
28  *  http://www.collectionspace.org
29  *  http://wiki.collectionspace.org
30
31  *  Copyright 2009 University of California at Berkeley
32
33  *  Licensed under the Educational Community License (ECL), Version 2.0.
34  *  You may not use this file except in compliance with this License.
35
36  *  You may obtain a copy of the ECL 2.0 License at
37
38  *  https://source.collectionspace.org/collection-space/LICENSE.txt
39
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.
45  */
46 /*
47  * To change this template, choose Tools | Templates
48  * and open the template in the editor.
49  */
50 package org.collectionspace.authentication.realm.db;
51
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;
58 import java.util.Map;
59 import java.util.Set;
60
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;
67
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;
73
74 /**
75  * CSpaceDbRealm provides access to user, password, role, tenant database
76  * @author 
77  */
78 public class CSpaceDbRealm implements CSpaceRealm {
79     private Logger logger = LoggerFactory.getLogger(CSpaceDbRealm.class);
80     
81     private String datasourceName;
82     private String principalsQuery;
83     private String rolesQuery;
84     private String tenantsQueryNoDisabled;
85     private String tenantsQueryWithDisabled;
86     private boolean suspendResume;
87
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";
91
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;
95         
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();
100                         try {
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));
105                         }
106                 }
107         }
108         
109         protected long getMaxRetrySeconds() {
110                 return this.maxRetrySeconds;
111         }
112         
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();
117                         try {
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));
122                         }
123                 }
124         }
125         
126         protected long getDelayBetweenAttemptsMillis() {
127                 return this.delayBetweenAttemptsMillis;
128         }
129     
130     /**
131      * CSpace Database Realm
132      * @param datasourceName datasource name
133      */
134     public CSpaceDbRealm(Map<String, ?> options) {
135         datasourceName = (String) options.get("dsJndiName");
136         if (datasourceName == null) {
137             datasourceName = "java:/DefaultDS";
138         }
139         Object tmp = options.get("principalsQuery");
140         if (tmp != null) {
141             principalsQuery = tmp.toString();
142         }
143         tmp = options.get("rolesQuery");
144         if (tmp != null) {
145             rolesQuery = tmp.toString();
146         }
147         tmp = options.get("tenantsQueryNoDisabled");
148         if (tmp != null) {
149             tenantsQueryNoDisabled = tmp.toString();
150         }
151         tmp = options.get("tenantsQueryWithDisabled");
152         if (tmp != null) {
153                 tenantsQueryWithDisabled = tmp.toString();
154         }
155         tmp = options.get("suspendResume");
156         if (tmp != null) {
157             suspendResume = Boolean.valueOf(tmp.toString()).booleanValue();
158         }
159         
160         this.setMaxRetrySeconds(options);
161         this.setDelayBetweenAttemptsMillis(options);
162         
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);
168         }
169
170     }
171
172     @Override
173     public String getPassword(String username) throws AccountException {
174
175         String password = null;
176         Connection conn = null;
177         PreparedStatement ps = null;
178         ResultSet rs = null;
179         try {
180             conn = getConnection();
181             // Get the password
182             if (logger.isDebugEnabled()) {
183                 logger.debug("Executing query: " + principalsQuery + ", with username: " + username);
184             }
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");
191                 }
192                 throw new AccountNotFoundException("No matching username found");
193             }
194
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);
199             }
200             AccountException ae = new AccountException("Authentication query failed: " + ex.getLocalizedMessage());
201             ae.initCause(ex);
202             throw ae;
203         } catch (AccountNotFoundException ex) {
204             throw ex;
205         } catch (Exception ex) {
206             AccountException ae = new AccountException("Unknown Exception");
207             ae.initCause(ex);
208             throw ae;
209         } finally {
210             if (rs != null) {
211                 try {
212                     rs.close();
213                 } catch (SQLException e) {
214                 }
215             }
216             if (ps != null) {
217                 try {
218                     ps.close();
219                 } catch (SQLException e) {
220                 }
221             }
222             if (conn != null) {
223                 try {
224                     conn.close();
225                 } catch (SQLException ex) {
226                 }
227             }
228         }
229         return password;
230     }
231
232     @Override
233     public Set<String> getRoles(String username) throws AccountException {
234         if (logger.isDebugEnabled()) {
235             logger.debug("getRoleSets using rolesQuery: " + rolesQuery + ", username: " + username);
236         }
237
238         Set<String> roles = new LinkedHashSet<String>();
239
240         Connection conn = null;
241         PreparedStatement ps = null;
242         ResultSet rs = null;
243
244         try {
245             conn = getConnection();
246             // Get the user role names
247             if (logger.isDebugEnabled()) {
248                 logger.debug("Executing query: " + rolesQuery + ", with username: " + username);
249             }
250
251             ps = conn.prepareStatement(rolesQuery);
252             try {
253                 ps.setString(1, username);
254             } catch (ArrayIndexOutOfBoundsException ignore) {
255                 // The query may not have any parameters so just try it
256             }
257             rs = ps.executeQuery();
258             if (rs.next() == false) {
259                 if (logger.isDebugEnabled()) {
260                     logger.debug("No roles found");
261                 }
262                 
263                 return roles;
264             }
265
266             do {
267                 String roleName = rs.getString(1);
268                 roles.add(roleName);
269                 
270             } while (rs.next());
271         } catch (SQLException ex) {
272             AccountException ae = new AccountException("Query failed");
273             ae.initCause(ex);
274             throw ae;
275         } catch (Exception e) {
276             AccountException ae = new AccountException("unknown exception");
277             ae.initCause(e);
278             throw ae;
279         } finally {
280             if (rs != null) {
281                 try {
282                     rs.close();
283                 } catch (SQLException e) {
284                 }
285             }
286             if (ps != null) {
287                 try {
288                     ps.close();
289                 } catch (SQLException e) {
290                 }
291             }
292             if (conn != null) {
293                 try {
294                     conn.close();
295                 } catch (Exception ex) {
296                 }
297             }
298
299         }
300
301         return roles;
302
303     }
304     @Override
305     public Set<CSpaceTenant> getTenants(String username) throws AccountException {
306         return getTenants(username, false);
307     }
308     
309     private boolean userIsTenantManager(Connection conn, String username) {
310         String acctQuery = "SELECT csid FROM accounts_common WHERE userid=?";
311         PreparedStatement ps = null;
312         ResultSet rs = null;
313         boolean accountIsTenantManager = false;
314         try {
315             ps = conn.prepareStatement(acctQuery);
316             ps.setString(1, username);
317             rs = ps.executeQuery();
318             if (rs.next()) {
319                 String acctCSID = rs.getString(1);
320                 if(AuthN.TENANT_MANAGER_ACCT_ID.equals(acctCSID)) {
321                         accountIsTenantManager = true;
322                 }
323             }
324         } catch (SQLException ex) {
325             if(logger.isDebugEnabled()) {
326                 logger.debug("userIsTenantManager query failed on SQL error: " + ex.getLocalizedMessage());
327             }
328         } catch (Exception e) {
329             if(logger.isDebugEnabled()) {
330                 logger.debug("userIsTenantManager unknown error: " + e.getLocalizedMessage());
331             }
332         } finally {
333             if (rs != null) {
334                 try {
335                     rs.close();
336                 } catch (SQLException e) {
337                 }
338             }
339             if (ps != null) {
340                 try {
341                     ps.close();
342                 } catch (SQLException e) {
343                 }
344             }
345         }
346         return accountIsTenantManager;
347     }
348     
349     /**
350      * Execute the tenantsQuery against the datasourceName to obtain the tenants for
351      * the authenticated user.
352      * @return set containing the roles
353      */
354     @Override
355     public Set<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException {
356
357         String tenantsQuery = getTenantQuery(includeDisabledTenants);
358         
359         if (logger.isDebugEnabled()) {
360             logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username);
361         }
362
363         Set<CSpaceTenant> tenants = new LinkedHashSet<CSpaceTenant>();
364         
365         Connection conn = null;
366         PreparedStatement ps = null;
367         ResultSet rs = null;
368
369         try {
370             conn = getConnection();
371
372             ps = conn.prepareStatement(tenantsQuery);
373             try {
374                 ps.setString(1, username);
375             } catch (ArrayIndexOutOfBoundsException ignore) {
376                 // The query may not have any parameters so just try it
377             }
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");
384                     }
385                     
386                     tenants.add(new CSpaceTenant(AuthN.TENANT_MANAGER_ACCT_ID, "PseudoTenant"));
387                 } else {
388                     if (logger.isDebugEnabled()) {
389                         logger.debug("No tenants found");
390                     }
391                     // We are running with an unauthenticatedIdentity so return an
392                     // empty Tenants set.
393                     // FIXME  should this be allowed?
394                 }
395                 
396                 return tenants;
397             }
398
399             do {
400                 String tenantId = rs.getString(1);
401                 String tenantName = rs.getString(2);
402
403                 tenants.add(new CSpaceTenant(tenantId, tenantName));
404             } while (rs.next());
405         } catch (SQLException ex) {
406             AccountException ae = new AccountException("Query failed");
407             ae.initCause(ex);
408             throw ae;
409         } catch (Exception e) {
410             AccountException ae = new AccountException("unknown exception");
411             ae.initCause(e);
412             throw ae;
413         } finally {
414             if (rs != null) {
415                 try {
416                     rs.close();
417                 } catch (SQLException e) {
418                 }
419             }
420             if (ps != null) {
421                 try {
422                     ps.close();
423                 } catch (SQLException e) {
424                 }
425             }
426             if (conn != null) {
427                 try {
428                     conn.close();
429                 } catch (Exception ex) {
430                 }
431             }
432         }
433
434         return tenants;
435     }
436
437     /*
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.
441      */
442     private Connection getConnection() throws Exception {
443         Connection result = null;
444                 boolean failed = true;
445                 Exception lastException = null;
446                 int requestAttempts = 0;
447
448                 long quittingTime = System.currentTimeMillis() + getMaxRetrySeconds() * 1000; // This is how long we attempt retries
449                 do {
450                         if (requestAttempts > 0) {
451                                 Thread.sleep(getDelayBetweenAttemptsMillis()); // Wait a little time between reattempts.
452                         }
453                         
454                         try {
455                                 // proceed to the original request by calling doFilter()
456                                 result = this.getConnection(getDataSourceName());
457                                 if (result != null) {
458                                         failed = false;
459                                         break; // the request was successfully executed, so we can break out of this retry loop
460                                 } else {
461                                         failed = true;
462                                         throw new ConnectException(); // The 'response' argument indicated a network related failure, so let's throw a generic connection exception
463                                 }
464                         } catch (Exception e) {
465                                 lastException = 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
469                                         break;
470                                 }
471                                 requestAttempts++; // keep track of how many times we've tried the request
472                         }
473                 } while (System.currentTimeMillis() < quittingTime);  // keep trying until we run out of time
474                 
475                 //
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.
478                 //
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.",
481                                         getDataSourceName(),
482                                         lastException.getClass().getName(),
483                                         requestAttempts));
484                 }
485
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.
488                         throw lastException;
489                 }
490                 
491                 return result;
492         }
493     
494         /*
495          * Don't call this method directly.  Instead, use the getConnection() method that take no arguments.
496          */
497         private Connection getConnection(String dataSourceName) throws AccountException, SQLException {
498         InitialContext ctx = null;
499         Connection conn = null;
500         DataSource ds = null;
501         
502         try {
503             ctx = new InitialContext();
504             try {
505                 ds = (DataSource) ctx.lookup(dataSourceName);
506             } catch (Exception e) {}
507             
508                 try {
509                         Context envCtx = (Context) ctx.lookup("java:comp/env");
510                         ds = (DataSource) envCtx.lookup(dataSourceName);
511                 } catch (Exception e) {}
512                 
513                 try {
514                         Context envCtx = (Context) ctx.lookup("java:comp");
515                         ds = (DataSource) envCtx.lookup(dataSourceName);
516                 } catch (Exception e) {}
517                 
518                 try {
519                         Context envCtx = (Context) ctx.lookup("java:");
520                         ds = (DataSource) envCtx.lookup(dataSourceName);
521                 } catch (Exception e) {}
522                 
523                 try {
524                         Context envCtx = (Context) ctx.lookup("java");
525                         ds = (DataSource) envCtx.lookup(dataSourceName);
526                 } catch (Exception e) {}
527                 
528                 try {
529                         ds = (DataSource) ctx.lookup("java:/" + dataSourceName);
530                 } catch (Exception e) {}  
531
532                 if (ds == null) {
533                 ds = AuthN.getDataSource();
534                 }
535                 
536             if (ds == null) {
537                 throw new IllegalArgumentException("datasource not found: " + dataSourceName);
538             }
539             
540             conn = ds.getConnection();
541             if (conn == null) {
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.
543             }
544             
545             return conn;
546             
547         } catch (NamingException ex) {
548             AccountException ae = new AccountException("Error looking up DataSource from: " + dataSourceName);
549             ae.initCause(ex);
550             throw ae;
551         } finally {
552             if (ctx != null) {
553                 try {
554                     ctx.close();
555                 } catch (Exception e) {
556                         e.printStackTrace();  // We should be using a logger here instead.
557                 }
558             }
559         }
560
561     }
562
563     /**
564      * @return the datasourceName
565      */
566     public String getDataSourceName() {
567         return datasourceName;
568     }
569
570     /**
571      * @return the principalQuery
572      */
573     public String getPrincipalQuery() {
574         return principalsQuery;
575     }
576
577     /**
578      * @param principalQuery the principalQuery to set
579      */
580     public void setPrincipalQuery(String principalQuery) {
581         this.principalsQuery = principalQuery;
582     }
583
584     /**
585      * @return the roleQuery
586      */
587     public String getRoleQuery() {
588         return rolesQuery;
589     }
590
591     /**
592      * @param roleQuery the roleQuery to set
593      */
594     public void setRoleQuery(String roleQuery) {
595         this.rolesQuery = roleQuery;
596     }
597
598     /**
599      * @return the tenantQuery
600      */
601     public String getTenantQuery(boolean includeDisabledTenants) {
602         return includeDisabledTenants?tenantsQueryWithDisabled:tenantsQueryNoDisabled;
603     }
604
605     /**
606      * @param tenantQuery the tenantQuery to set
607     public void setTenantQuery(String tenantQuery) {
608         this.tenantsQueryNoDisabled = tenantQuery;
609     }
610      */
611     
612     /*
613      * This method crawls the exception chain looking for network related exceptions and
614      * returns 'true' if it finds one.
615      */
616         public static boolean exceptionChainContainsNetworkError(Throwable exceptionChain) {
617                 boolean result = false;
618                 Throwable cause = exceptionChain;
619
620                 while (cause != null) {
621                         if (isExceptionNetworkRelated(cause) == true) {
622                                 result = true;
623                                 break;
624                         }
625                         
626                         cause = cause.getCause();
627                 }
628
629                 return result;
630         }
631         
632         /*
633          * Return 'true' if the exception is in the "java.net" package.
634          */
635         private static boolean isExceptionNetworkRelated(Throwable cause) {
636                 boolean result = false;
637
638                 String className = cause.getClass().getCanonicalName();
639                 if (className.contains("java.net") == true) {
640                         result = true;
641                 }
642
643                 return result;
644         }
645     
646 }