]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
62045711143b0b4903b4cabdc555f683d0b65cdf
[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.postgresql.util.PSQLState;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * CSpaceDbRealm provides access to user, password, role, tenant database
77  * @author 
78  */
79 public class CSpaceDbRealm implements CSpaceRealm {
80     private Logger logger = LoggerFactory.getLogger(CSpaceDbRealm.class);
81     
82     private String datasourceName;
83     private String principalsQuery;
84     private String saltQuery;
85     private String rolesQuery;
86     private String tenantsQueryNoDisabled;
87     private String tenantsQueryWithDisabled;
88     private boolean suspendResume;
89
90     private long maxRetrySeconds = MAX_RETRY_SECONDS;
91     private static final int MAX_RETRY_SECONDS = 5;
92     private static final String MAX_RETRY_SECONDS_STR = "maxRetrySeconds";
93
94         private long delayBetweenAttemptsMillis = DELAY_BETWEEN_ATTEMPTS_MILLISECONDS;
95     private static final String DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR = "delayBetweenAttemptsMillis";
96         private static final long DELAY_BETWEEN_ATTEMPTS_MILLISECONDS = 200;
97         
98         protected void setMaxRetrySeconds(Map<String, ?> options) {
99                 Object optionsObj = options.get(MAX_RETRY_SECONDS_STR);
100                 if (optionsObj != null) {
101                         String paramValue = optionsObj.toString();
102                         try {
103                                 maxRetrySeconds = Long.parseLong(paramValue);
104                         } catch (NumberFormatException e) {
105                                 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.",
106                                                 MAX_RETRY_SECONDS_STR, paramValue, maxRetrySeconds));
107                         }
108                 }
109         }
110         
111         protected long getMaxRetrySeconds() {
112                 return this.maxRetrySeconds;
113         }
114         
115         protected void setDelayBetweenAttemptsMillis(Map<String, ?> options) {
116                 Object optionsObj = options.get(DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR);
117                 if (optionsObj != null) {
118                         String paramValue = optionsObj.toString();
119                         try {
120                                 delayBetweenAttemptsMillis = Long.parseLong(paramValue);
121                         } catch (NumberFormatException e) {
122                                 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.",
123                                                 MAX_RETRY_SECONDS_STR, paramValue, delayBetweenAttemptsMillis));
124                         }
125                 }
126         }
127         
128         protected long getDelayBetweenAttemptsMillis() {
129                 return this.delayBetweenAttemptsMillis;
130         }
131     
132     /**
133      * CSpace Database Realm
134      * @param datasourceName datasource name
135      */
136     public CSpaceDbRealm(Map<String, ?> options) {
137         datasourceName = (String) options.get("dsJndiName");
138         if (datasourceName == null) {
139             datasourceName = "java:/DefaultDS";
140         }
141         Object tmp = options.get("principalsQuery");
142         if (tmp != null) {
143             principalsQuery = tmp.toString();
144         }
145         tmp = options.get("saltQuery");
146         if (tmp != null) {
147                 saltQuery = tmp.toString();
148         }
149         tmp = options.get("rolesQuery");
150         if (tmp != null) {
151             rolesQuery = tmp.toString();
152         }
153         tmp = options.get("tenantsQueryNoDisabled");
154         if (tmp != null) {
155             tenantsQueryNoDisabled = tmp.toString();
156         }
157         tmp = options.get("tenantsQueryWithDisabled");
158         if (tmp != null) {
159                 tenantsQueryWithDisabled = tmp.toString();
160         }
161         tmp = options.get("suspendResume");
162         if (tmp != null) {
163             suspendResume = Boolean.valueOf(tmp.toString()).booleanValue();
164         }
165         
166         this.setMaxRetrySeconds(options);
167         this.setDelayBetweenAttemptsMillis(options);
168         
169         if (logger.isTraceEnabled()) {
170             logger.trace("DatabaseServerLoginModule, dsJndiName=" + datasourceName);
171             logger.trace("principalsQuery=" + principalsQuery);
172             logger.trace("rolesQuery=" + rolesQuery);
173             logger.trace("suspendResume=" + suspendResume);
174         }
175
176     }
177
178     @Override
179     public String getPassword(String username) throws AccountException {
180
181         String password = null;
182         Connection conn = null;
183         PreparedStatement ps = null;
184         ResultSet rs = null;
185         try {
186             conn = getConnection();
187             // Get the password
188             if (logger.isDebugEnabled()) {
189                 logger.debug("Executing query: " + principalsQuery + ", with username: " + username);
190             }
191             ps = conn.prepareStatement(principalsQuery);
192             ps.setString(1, username);
193             rs = ps.executeQuery();
194             if (rs.next() == false) {
195                 if (logger.isDebugEnabled()) {
196                     logger.debug(principalsQuery + " returned no matches from db");
197                 }
198                 throw new AccountNotFoundException("No matching username found");
199             }
200
201             password = rs.getString(1);
202         } catch (SQLException ex) {
203             if (logger.isTraceEnabled() == true) {
204                 logger.error("Could not open database to read AuthN tables.", ex);
205             }
206             AccountException ae = new AccountException("Authentication query failed: " + ex.getLocalizedMessage());
207             ae.initCause(ex);
208             throw ae;
209         } catch (AccountNotFoundException ex) {
210             throw ex;
211         } catch (Exception ex) {
212             AccountException ae = new AccountException("Unknown Exception");
213             ae.initCause(ex);
214             throw ae;
215         } finally {
216             if (rs != null) {
217                 try {
218                     rs.close();
219                 } catch (SQLException e) {
220                 }
221             }
222             if (ps != null) {
223                 try {
224                     ps.close();
225                 } catch (SQLException e) {
226                 }
227             }
228             if (conn != null) {
229                 try {
230                     conn.close();
231                 } catch (SQLException ex) {
232                 }
233             }
234         }
235         return password;
236     }
237
238     @Override
239     public Set<String> getRoles(String username) throws AccountException {
240         if (logger.isDebugEnabled()) {
241             logger.debug("getRoleSets using rolesQuery: " + rolesQuery + ", username: " + username);
242         }
243
244         Set<String> roles = new LinkedHashSet<String>();
245
246         Connection conn = null;
247         PreparedStatement ps = null;
248         ResultSet rs = null;
249
250         try {
251             conn = getConnection();
252             // Get the user role names
253             if (logger.isDebugEnabled()) {
254                 logger.debug("Executing query: " + rolesQuery + ", with username: " + username);
255             }
256
257             ps = conn.prepareStatement(rolesQuery);
258             try {
259                 ps.setString(1, username);
260             } catch (ArrayIndexOutOfBoundsException ignore) {
261                 // The query may not have any parameters so just try it
262             }
263             rs = ps.executeQuery();
264             if (rs.next() == false) {
265                 if (logger.isDebugEnabled()) {
266                     logger.debug("No roles found");
267                 }
268                 
269                 return roles;
270             }
271
272             do {
273                 String roleName = rs.getString(1);
274                 roles.add(roleName);
275                 
276             } while (rs.next());
277         } catch (SQLException ex) {
278             AccountException ae = new AccountException("Query failed");
279             ae.initCause(ex);
280             throw ae;
281         } catch (Exception e) {
282             AccountException ae = new AccountException("unknown exception");
283             ae.initCause(e);
284             throw ae;
285         } finally {
286             if (rs != null) {
287                 try {
288                     rs.close();
289                 } catch (SQLException e) {
290                 }
291             }
292             if (ps != null) {
293                 try {
294                     ps.close();
295                 } catch (SQLException e) {
296                 }
297             }
298             if (conn != null) {
299                 try {
300                     conn.close();
301                 } catch (Exception ex) {
302                 }
303             }
304
305         }
306
307         return roles;
308
309     }
310     @Override
311     public Set<CSpaceTenant> getTenants(String username) throws AccountException {
312         return getTenants(username, false);
313     }
314     
315     private boolean userIsTenantManager(Connection conn, String username) {
316         String acctQuery = "SELECT csid FROM accounts_common WHERE userid=?";
317         PreparedStatement ps = null;
318         ResultSet rs = null;
319         boolean accountIsTenantManager = false;
320         try {
321             ps = conn.prepareStatement(acctQuery);
322             ps.setString(1, username);
323             rs = ps.executeQuery();
324             if (rs.next()) {
325                 String acctCSID = rs.getString(1);
326                 if(AuthN.TENANT_MANAGER_ACCT_ID.equals(acctCSID)) {
327                         accountIsTenantManager = true;
328                 }
329             }
330         } catch (SQLException ex) {
331             if(logger.isDebugEnabled()) {
332                 logger.debug("userIsTenantManager query failed on SQL error: " + ex.getLocalizedMessage());
333             }
334         } catch (Exception e) {
335             if(logger.isDebugEnabled()) {
336                 logger.debug("userIsTenantManager unknown error: " + e.getLocalizedMessage());
337             }
338         } finally {
339             if (rs != null) {
340                 try {
341                     rs.close();
342                 } catch (SQLException e) {
343                 }
344             }
345             if (ps != null) {
346                 try {
347                     ps.close();
348                 } catch (SQLException e) {
349                 }
350             }
351         }
352         return accountIsTenantManager;
353     }
354     
355     /**
356      * Execute the tenantsQuery against the datasourceName to obtain the tenants for
357      * the authenticated user.
358      * @return set containing the roles
359      */
360     @Override
361     public Set<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException {
362
363         String tenantsQuery = getTenantQuery(includeDisabledTenants);
364         
365         if (logger.isDebugEnabled()) {
366             logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username);
367         }
368
369         Set<CSpaceTenant> tenants = new LinkedHashSet<CSpaceTenant>();
370         
371         Connection conn = null;
372         PreparedStatement ps = null;
373         ResultSet rs = null;
374
375         try {
376             conn = getConnection();
377
378             ps = conn.prepareStatement(tenantsQuery);
379             try {
380                 ps.setString(1, username);
381             } catch (ArrayIndexOutOfBoundsException ignore) {
382                 // The query may not have any parameters so just try it
383             }
384             rs = ps.executeQuery();
385             if (rs.next() == false) {
386                 // Check for the tenantManager
387                 if(userIsTenantManager(conn, username)) {
388                     if (logger.isDebugEnabled()) {
389                         logger.debug("GetTenants called with tenantManager - synthesizing the pseudo-tenant");
390                     }
391                     
392                     tenants.add(new CSpaceTenant(AuthN.TENANT_MANAGER_ACCT_ID, "PseudoTenant"));
393                 } else {
394                     if (logger.isDebugEnabled()) {
395                         logger.debug("No tenants found");
396                     }
397                     // We are running with an unauthenticatedIdentity so return an
398                     // empty Tenants set.
399                     // FIXME  should this be allowed?
400                 }
401                 
402                 return tenants;
403             }
404
405             do {
406                 String tenantId = rs.getString(1);
407                 String tenantName = rs.getString(2);
408
409                 tenants.add(new CSpaceTenant(tenantId, tenantName));
410             } while (rs.next());
411         } catch (SQLException ex) {
412             AccountException ae = new AccountException("Query failed");
413             ae.initCause(ex);
414             throw ae;
415         } catch (Exception e) {
416             AccountException ae = new AccountException("unknown exception");
417             ae.initCause(e);
418             throw ae;
419         } finally {
420             if (rs != null) {
421                 try {
422                     rs.close();
423                 } catch (SQLException e) {
424                 }
425             }
426             if (ps != null) {
427                 try {
428                     ps.close();
429                 } catch (SQLException e) {
430                 }
431             }
432             if (conn != null) {
433                 try {
434                     conn.close();
435                 } catch (Exception ex) {
436                 }
437             }
438         }
439
440         return tenants;
441     }
442
443     /*
444      * This method will attempt to get a connection.  If a network error prevents it from getting a connection on the first try
445      * it will retry for the next 'getMaxRetrySeconds()' seconds.  If it is unable to get the connection then it will timeout and
446      * throw an exception.
447      */
448     private Connection getConnection() throws Exception {
449         Connection result = null;
450                 boolean failed = true;
451                 Exception lastException = null;
452                 int requestAttempts = 0;
453
454                 long quittingTime = System.currentTimeMillis() + getMaxRetrySeconds() * 1000; // This is how long we attempt retries
455                 do {
456                         if (requestAttempts > 0) {
457                                 Thread.sleep(getDelayBetweenAttemptsMillis()); // Wait a little time between reattempts.
458                         }
459                         
460                         try {
461                                 // proceed to the original request by calling doFilter()
462                                 result = this.getConnection(getDataSourceName());
463                                 if (result != null) {
464                                         failed = false;
465                                         break; // the request was successfully executed, so we can break out of this retry loop
466                                 } else {
467                                         failed = true;
468                                         throw new ConnectException(); // The 'response' argument indicated a network related failure, so let's throw a generic connection exception
469                                 }
470                         } catch (Exception e) {
471                                 lastException = e;
472                                 if (exceptionChainContainsNetworkError(lastException) == false) {
473                                         // Break if the exception chain does not contain a
474                                         // network related exception because we don't want to retry if it's not a network related failure
475                                         break;
476                                 }
477                                 requestAttempts++; // keep track of how many times we've tried the request
478                         }
479                 } while (System.currentTimeMillis() < quittingTime);  // keep trying until we run out of time
480                 
481                 //
482                 // Add a warning to the logs if we encountered *any* failures on our re-attempts.  Only add the warning
483                 // if we were eventually successful.
484                 //
485                 if (requestAttempts > 0 && failed == false) {
486                         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.",
487                                         getDataSourceName(),
488                                         lastException.getClass().getName(),
489                                         requestAttempts));
490                 }
491
492                 if (failed == true) {
493                         // If we get here, it means all of our attempts to get a successful call to chain.doFilter() have failed.
494                         throw lastException;
495                 }
496                 
497                 return result;
498         }
499     
500         /*
501          * Don't call this method directly.  Instead, use the getConnection() method that take no arguments.
502          */
503         private Connection getConnection(String dataSourceName) throws AccountException, SQLException {
504         InitialContext ctx = null;
505         Connection conn = null;
506         DataSource ds = null;
507         
508         try {
509             ctx = new InitialContext();
510             try {
511                 ds = (DataSource) ctx.lookup(dataSourceName);
512             } catch (Exception e) {}
513             
514                 try {
515                         Context envCtx = (Context) ctx.lookup("java:comp/env");
516                         ds = (DataSource) envCtx.lookup(dataSourceName);
517                 } catch (Exception e) {}
518                 
519                 try {
520                         Context envCtx = (Context) ctx.lookup("java:comp");
521                         ds = (DataSource) envCtx.lookup(dataSourceName);
522                 } catch (Exception e) {}
523                 
524                 try {
525                         Context envCtx = (Context) ctx.lookup("java:");
526                         ds = (DataSource) envCtx.lookup(dataSourceName);
527                 } catch (Exception e) {}
528                 
529                 try {
530                         Context envCtx = (Context) ctx.lookup("java");
531                         ds = (DataSource) envCtx.lookup(dataSourceName);
532                 } catch (Exception e) {}
533                 
534                 try {
535                         ds = (DataSource) ctx.lookup("java:/" + dataSourceName);
536                 } catch (Exception e) {}  
537
538                 if (ds == null) {
539                 ds = AuthN.getDataSource();
540                 }
541                 
542             if (ds == null) {
543                 throw new IllegalArgumentException("datasource not found: " + dataSourceName);
544             }
545             
546             conn = ds.getConnection();
547             if (conn == null) {
548                 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.
549             }
550             
551             return conn;
552             
553         } catch (NamingException ex) {
554             AccountException ae = new AccountException("Error looking up DataSource from: " + dataSourceName);
555             ae.initCause(ex);
556             throw ae;
557         } finally {
558             if (ctx != null) {
559                 try {
560                     ctx.close();
561                 } catch (Exception e) {
562                         e.printStackTrace();  // We should be using a logger here instead.
563                 }
564             }
565         }
566
567     }
568
569     /**
570      * @return the datasourceName
571      */
572     public String getDataSourceName() {
573         return datasourceName;
574     }
575
576     /**
577      * @return the principalQuery
578      */
579     public String getPrincipalQuery() {
580         return principalsQuery;
581     }
582
583     /**
584      * @param principalQuery the principalQuery to set
585      */
586     public void setPrincipalQuery(String principalQuery) {
587         this.principalsQuery = principalQuery;
588     }
589
590     /**
591      * @return the roleQuery
592      */
593     public String getRoleQuery() {
594         return rolesQuery;
595     }
596
597     /**
598      * @param roleQuery the roleQuery to set
599      */
600     public void setRoleQuery(String roleQuery) {
601         this.rolesQuery = roleQuery;
602     }
603
604     /**
605      * @return the tenantQuery
606      */
607     public String getTenantQuery(boolean includeDisabledTenants) {
608         return includeDisabledTenants?tenantsQueryWithDisabled:tenantsQueryNoDisabled;
609     }
610
611     /**
612      * @param tenantQuery the tenantQuery to set
613     public void setTenantQuery(String tenantQuery) {
614         this.tenantsQueryNoDisabled = tenantQuery;
615     }
616      */
617     
618     /*
619      * This method crawls the exception chain looking for network related exceptions and
620      * returns 'true' if it finds one.
621      */
622         public static boolean exceptionChainContainsNetworkError(Throwable exceptionChain) {
623                 boolean result = false;
624                 Throwable cause = exceptionChain;
625
626                 while (cause != null) {
627                         if (isExceptionNetworkRelated(cause) == true) {
628                                 result = true;
629                                 break;
630                         }
631                         
632                         cause = cause.getCause();
633                 }
634
635                 return result;
636         }
637         
638         /*
639          * Return 'true' if the exception is in the "java.net" package.
640          */
641         private static boolean isExceptionNetworkRelated(Throwable cause) {
642                 boolean result = false;
643
644                 String className = cause.getClass().getCanonicalName();
645                 if (className.contains("java.net") == true) {
646                         result = true;
647                 }
648
649                 return result;
650         }
651
652         @Override
653         public String getSalt(String username) throws AccountException {
654         String salt = null;
655         Connection conn = null;
656         PreparedStatement ps = null;
657         ResultSet rs = null;
658         try {
659             conn = getConnection();
660             // Get the salt
661             if (logger.isDebugEnabled()) {
662                 logger.debug("Executing query: " + saltQuery + ", with username: " + username);
663             }
664             ps = conn.prepareStatement(saltQuery);
665             ps.setString(1, username);
666             rs = ps.executeQuery();
667             if (rs.next() == false) {
668                 if (logger.isDebugEnabled()) {
669                     logger.debug(saltQuery + " returned no matches from db");
670                 }
671                 throw new AccountNotFoundException("No matching username found");
672             }
673
674             salt = rs.getString(1);
675         } catch (SQLException ex) {
676                 // Assuming PostgreSQL
677             if (PSQLState.UNDEFINED_COLUMN.getState().equals(ex.getSQLState())) {
678                 String msg = "'USERS' table is missing 'salt' column for password encyrption.  Assuming existing passwords are unsalted.";
679                 logger.warn(msg);
680             } else {
681                 AccountException ae = new AccountException("Authentication query failed: " + ex.getLocalizedMessage());
682                 ae.initCause(ex);
683                 throw ae;
684             }
685         } catch (AccountNotFoundException ex) {
686             throw ex;
687         } catch (Exception ex) {
688             AccountException ae = new AccountException("Unknown Exception");
689             ae.initCause(ex);
690             throw ae;
691         } finally {
692             if (rs != null) {
693                 try {
694                     rs.close();
695                 } catch (SQLException e) {
696                 }
697             }
698             if (ps != null) {
699                 try {
700                     ps.close();
701                 } catch (SQLException e) {
702                 }
703             }
704             if (conn != null) {
705                 try {
706                     conn.close();
707                 } catch (SQLException ex) {
708                 }
709             }
710         }
711         
712         return salt;
713     }
714     
715 }