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