001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package org.apache.geronimo.tomcat.realm;
018    
019    import org.apache.catalina.Context;
020    import org.apache.catalina.LifecycleException;
021    import org.apache.catalina.Wrapper;
022    import org.apache.catalina.connector.Request;
023    import org.apache.catalina.connector.Response;
024    import org.apache.catalina.deploy.LoginConfig;
025    import org.apache.catalina.deploy.SecurityConstraint;
026    import org.apache.catalina.realm.JAASRealm;
027    import org.apache.commons.logging.Log;
028    import org.apache.commons.logging.LogFactory;
029    import org.apache.geronimo.security.ContextManager;
030    import org.apache.geronimo.security.jacc.PolicyContextHandlerContainerSubject;
031    import org.apache.geronimo.security.realm.providers.CertificateChainCallbackHandler;
032    import org.apache.geronimo.security.realm.providers.PasswordCallbackHandler;
033    import org.apache.geronimo.tomcat.JAASTomcatPrincipal;
034    
035    import javax.security.auth.Subject;
036    import javax.security.auth.callback.CallbackHandler;
037    import javax.security.auth.login.AccountExpiredException;
038    import javax.security.auth.login.CredentialExpiredException;
039    import javax.security.auth.login.FailedLoginException;
040    import javax.security.auth.login.LoginContext;
041    import javax.security.auth.login.LoginException;
042    import javax.security.jacc.PolicyContext;
043    import javax.security.jacc.PolicyContextException;
044    import javax.security.jacc.WebResourcePermission;
045    import javax.security.jacc.WebRoleRefPermission;
046    import javax.security.jacc.WebUserDataPermission;
047    import javax.servlet.ServletRequest;
048    
049    import java.io.IOException;
050    import java.security.AccessControlContext;
051    import java.security.AccessControlException;
052    import java.security.Principal;
053    import java.security.cert.X509Certificate;
054    
055    
056    public class TomcatGeronimoRealm extends JAASRealm {
057    
058        private static final Log log = LogFactory.getLog(TomcatGeronimoRealm.class);
059    
060        private static ThreadLocal currentRequestWrapperName = new ThreadLocal();
061    
062        /**
063         * Descriptive information about this <code>Realm</code> implementation.
064         */
065        protected static final String info = "org.apache.geronimo.tomcat.TomcatGeronimoRealm/1.0";
066    
067        /**
068         * Descriptive information about this <code>Realm</code> implementation.
069         */
070        protected static final String name = "TomcatGeronimoRealm";
071    
072        public TomcatGeronimoRealm() {
073    
074         }
075    
076        public static String setRequestWrapperName(String requestWrapperName) {
077            String old = (String) currentRequestWrapperName.get();
078            currentRequestWrapperName.set(requestWrapperName);
079            return old;
080        }
081    
082        /**
083         * Enforce any user data constraint required by the security constraint
084         * guarding this request URI.  Return <code>true</code> if this constraint
085         * was not violated and processing should continue, or <code>false</code>
086         * if we have created a response already.
087         *
088         * @param request     Request we are processing
089         * @param response    Response we are creating
090         * @param constraints Security constraint being checked
091         * @throws IOException if an input/output error occurs
092         */
093        public boolean hasUserDataPermission(Request request,
094                                             Response response,
095                                             SecurityConstraint[] constraints)
096                throws IOException {
097    
098            //Get an authenticated subject, if there is one
099            Subject subject = null;
100            try {
101    
102                //We will use the PolicyContextHandlerContainerSubject.HANDLER_KEY to see if a user
103                //has authenticated, since a request.getUserPrincipal() will not pick up the user
104                //unless its using a cached session.
105                subject = (Subject) PolicyContext.getContext(PolicyContextHandlerContainerSubject.HANDLER_KEY);
106    
107            } catch (PolicyContextException e) {
108                log.error(e);
109            }
110    
111            //If nothing has authenticated yet, do the normal
112            if (subject == null)
113                return super.hasUserDataPermission(request, response, constraints);
114    
115            ContextManager.setCallers(subject, subject);
116    
117            try {
118    
119                AccessControlContext acc = ContextManager.getCurrentContext();
120    
121                /**
122                 * JACC v1.0 secion 4.1.1
123                 */
124                WebUserDataPermission wudp = new WebUserDataPermission(request);
125                acc.checkPermission(wudp);
126    
127            } catch (AccessControlException ace) {
128                response.sendError(Response.SC_FORBIDDEN);
129                return false;
130            }
131    
132            return true;
133        }
134    
135        /**
136         * Perform access control based on the specified authorization constraint.
137         * Return <code>true</code> if this constraint is satisfied and processing
138         * should continue, or <code>false</code> otherwise.
139         *
140         * @param request    Request we are processing
141         * @param response   Response we are creating
142         * @param constraints Security constraints we are enforcing
143         * @param context    The Context to which client of this class is attached.
144         * @throws java.io.IOException if an input/output error occurs
145         */
146        public boolean hasResourcePermission(Request request,
147                                             Response response,
148                                             SecurityConstraint[] constraints,
149                                             Context context)
150                throws IOException {
151    
152            // Specifically allow access to the form login and form error pages
153            // and the "j_security_check" action
154            LoginConfig config = context.getLoginConfig();
155            if ((config != null) &&
156                (org.apache.catalina.realm.Constants.FORM_METHOD.equals(config.getAuthMethod()))) {
157                String requestURI = request.getDecodedRequestURI();
158                String loginPage = context.getPath() + config.getLoginPage();
159                if (loginPage.equals(requestURI)) {
160                    if (log.isDebugEnabled())
161                        log.debug(" Allow access to login page " + loginPage);
162                    return (true);
163                }
164                String errorPage = context.getPath() + config.getErrorPage();
165                if (errorPage.equals(requestURI)) {
166                    if (log.isDebugEnabled())
167                        log.debug(" Allow access to error page " + errorPage);
168                    return (true);
169                }
170                if (requestURI.endsWith(org.apache.catalina.realm.Constants.FORM_ACTION)) {
171                    if (log.isDebugEnabled())
172                        log.debug(" Allow access to username/password submission");
173                    return (true);
174                }
175            }
176            
177            //Set the current wrapper name (Servlet mapping)
178            currentRequestWrapperName.set(request.getWrapper().getName());
179    
180            // Which user principal have we already authenticated?
181            Principal principal = request.getUserPrincipal();
182    
183            //If we have no principal, then we should use the default.
184            if (principal == null) {
185                return request.isSecure();
186    
187            } else {
188                Subject currentCaller = ((JAASTomcatPrincipal) principal).getSubject();
189                ContextManager.setCallers(currentCaller, currentCaller);
190            }
191    
192            try {
193    
194                AccessControlContext acc = ContextManager.getCurrentContext();
195    
196    
197                /**
198                 * JACC v1.0 section 4.1.2
199                 */
200                acc.checkPermission(new WebResourcePermission(request));
201    
202            } catch (AccessControlException ace) {
203                response.sendError(Response.SC_FORBIDDEN);
204                return false;
205            }
206    
207            return true;
208    
209        }
210    
211        /**
212         * Return <code>true</code> if the specified Principal has the specified
213         * security role, within the context of this Realm; otherwise return
214         * <code>false</code>.
215         *
216         * @param principal Principal for whom the role is to be checked
217         * @param role      Security role to be checked
218         */
219        public boolean hasRole(Principal principal, String role) {
220    
221            if ((principal == null) || (role == null) || !(principal instanceof JAASTomcatPrincipal)) {
222                return false;
223            }
224    
225            String name = (String)currentRequestWrapperName.get();
226    
227            /**
228             * JACC v1.0 secion B.19
229             */
230            if (name == null || name.equals("jsp")) {
231                name = "";
232            }
233    
234            //Set the caller
235            Subject currentCaller = ((JAASTomcatPrincipal) principal).getSubject();
236            ContextManager.setCallers(currentCaller, currentCaller);
237    
238            AccessControlContext acc = ContextManager.getCurrentContext();
239    
240            try {
241                /**
242                 * JACC v1.0 section 4.1.3
243                 */
244                acc.checkPermission(new WebRoleRefPermission(name, role));
245            } catch (AccessControlException e) {
246                return false;
247            }
248    
249            return true;
250        }
251    
252        /**
253         * Return the <code>Principal</code> associated with the specified
254         * username and credentials, if there is one; otherwise return
255         * <code>null</code>.
256         * <p/>
257         * If there are any errors with the JDBC connection, executing the query or
258         * anything we return null (don't authenticate). This event is also logged,
259         * and the connection will be closed so that a subsequent request will
260         * automatically re-open it.
261         *
262         * @param username    Username of the <code>Principal</code> to look up
263         * @param credentials Password or other credentials to use in authenticating this
264         *                    username
265         */
266        public Principal authenticate(String username, String credentials) {
267    
268            char[] cred = credentials == null? null: credentials.toCharArray();
269            CallbackHandler callbackHandler = new PasswordCallbackHandler(username, cred);
270            return authenticate(callbackHandler, username);
271        }
272    
273        public Principal authenticate(X509Certificate[] certs) {
274            if (certs == null || certs.length == 0) {
275                return null;
276            }
277            CallbackHandler callbackHandler = new CertificateChainCallbackHandler(certs);
278            String principalName = certs[0].getSubjectX500Principal().getName();
279            return authenticate(callbackHandler, principalName);
280        }
281    
282        public Principal authenticate(CallbackHandler callbackHandler, String principalName) {
283    
284            // Establish a LoginContext to use for authentication
285            try {
286    
287                if ( (principalName!=null) && (!principalName.equals("")) ) {
288                  LoginContext loginContext = null;
289                  if (appName == null)
290                      appName = "Tomcat";
291    
292                  if (log.isDebugEnabled())
293                      log.debug(sm.getString("jaasRealm.beginLogin", principalName, appName));
294    
295                  // What if the LoginModule is in the container class loader ?
296                  ClassLoader ocl = null;
297    
298                  if (isUseContextClassLoader()) {
299                      ocl = Thread.currentThread().getContextClassLoader();
300                      Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
301                  }
302    
303                  try {
304                      loginContext = new LoginContext(appName, callbackHandler);
305                  } catch (Throwable e) {
306                      log.error(sm.getString("jaasRealm.unexpectedError"), e);
307                      return (null);
308                  } finally {
309                      if (isUseContextClassLoader()) {
310                          Thread.currentThread().setContextClassLoader(ocl);
311                      }
312                  }
313    
314                  if (log.isDebugEnabled())
315                      log.debug("Login context created " + principalName);
316    
317                  // Negotiate a login via this LoginContext
318                  Subject subject;
319                  try {
320                      loginContext.login();
321                      Subject tempSubject = loginContext.getSubject();
322                      if (tempSubject == null) {
323                          if (log.isDebugEnabled())
324                              log.debug(sm.getString("jaasRealm.failedLogin", principalName));
325                          return (null);
326                      }
327    
328                      subject = ContextManager.getServerSideSubject(tempSubject);
329                      if (subject == null) {
330                          if (log.isDebugEnabled())
331                              log.debug(sm.getString("jaasRealm.failedLogin", principalName));
332                          return (null);
333                      }
334    
335                      ContextManager.setCallers(subject, subject);
336    
337                  } catch (AccountExpiredException e) {
338                      if (log.isDebugEnabled())
339                          log.debug(sm.getString("jaasRealm.accountExpired", principalName));
340                      return (null);
341                  } catch (CredentialExpiredException e) {
342                      if (log.isDebugEnabled())
343                          log.debug(sm.getString("jaasRealm.credentialExpired", principalName));
344                      return (null);
345                  } catch (FailedLoginException e) {
346                      if (log.isDebugEnabled())
347                          log.debug(sm.getString("jaasRealm.failedLogin", principalName));
348                      return (null);
349                  } catch (LoginException e) {
350                      log.warn(sm.getString("jaasRealm.loginException", principalName), e);
351                      return (null);
352                  } catch (Throwable e) {
353                      log.error(sm.getString("jaasRealm.unexpectedError"), e);
354                      return (null);
355                  }
356    
357                  if (log.isDebugEnabled())
358                      log.debug(sm.getString("jaasRealm.loginContextCreated", principalName));
359    
360                  // Return the appropriate Principal for this authenticated Subject
361      /*            Principal principal = createPrincipal(username, subject);
362                  if (principal == null) {
363                      log.debug(sm.getString("jaasRealm.authenticateFailure", username));
364                      return (null);
365                  }
366                  if (log.isDebugEnabled()) {
367                      log.debug(sm.getString("jaasRealm.authenticateSuccess", username));
368                  }
369      */
370                  JAASTomcatPrincipal jaasPrincipal = new JAASTomcatPrincipal(principalName);
371                  jaasPrincipal.setSubject(subject);
372    
373                  return (jaasPrincipal);
374                }
375                else {
376                    if (log.isDebugEnabled())
377                        log.debug("Login Failed - null userID");
378                    return null;
379                }
380    
381            } catch (Throwable t) {
382                log.error("error ", t);
383                return null;
384            }
385        }
386        /**
387         * Prepare for active use of the public methods of this <code>Component</code>.
388         *
389         * @throws org.apache.catalina.LifecycleException
390         *          if this component detects a fatal error
391         *          that prevents it from being started
392         */
393        public void start() throws LifecycleException {
394    
395            // Perform normal superclass initialization
396            super.start();
397    
398        }
399    
400        /**
401         * Gracefully shut down active use of the public methods of this <code>Component</code>.
402         *
403         * @throws LifecycleException if this component detects a fatal error
404         *                            that needs to be reported
405         */
406        public void stop() throws LifecycleException {
407    
408            // Perform normal superclass finalization
409            super.stop();
410    
411        }
412    }