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