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