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            String name = currentRequestWrapperName.get();
220    
221            /**
222             * JACC v1.0 secion B.19
223             */
224            if (name == null || name.equals("jsp")) {
225                name = "";
226            }
227    
228            AccessControlContext acc = ContextManager.getCurrentContext();
229    
230            try {
231                /**
232                 * JACC v1.0 section 4.1.3
233                 */
234                acc.checkPermission(new WebRoleRefPermission(name, role));
235            } catch (AccessControlException e) {
236                return false;
237            }
238    
239            return true;
240        }
241    
242        /**
243         * Return the <code>Principal</code> associated with the specified
244         * username and credentials, if there is one; otherwise return
245         * <code>null</code>.
246         * <p/>
247         * If there are any errors with the JDBC connection, executing the query or
248         * anything we return null (don't authenticate). This event is also logged,
249         * and the connection will be closed so that a subsequent request will
250         * automatically re-open it.
251         *
252         * @param username    Username of the <code>Principal</code> to look up
253         * @param credentials Password or other credentials to use in authenticating this
254         *                    username
255         */
256        public Principal authenticate(String username, String credentials) {
257    
258            char[] cred = credentials == null ? null : credentials.toCharArray();
259            CallbackHandler callbackHandler = new PasswordCallbackHandler(username, cred);
260            return authenticate(callbackHandler, username);
261        }
262    
263        public Principal authenticate(X509Certificate[] certs) {
264            if (certs == null || certs.length == 0) {
265                return null;
266            }
267            CallbackHandler callbackHandler = new CertificateChainCallbackHandler(certs);
268            String principalName = certs[0].getSubjectX500Principal().getName();
269            return authenticate(callbackHandler, principalName);
270        }
271    
272        public Principal authenticate(CallbackHandler callbackHandler, String principalName) {
273    
274            // Establish a LoginContext to use for authentication
275            try {
276    
277                if ((principalName != null) && (!principalName.equals(""))) {
278                    LoginContext loginContext = null;
279                    if (appName == null)
280                        appName = "Tomcat";
281    
282                    if (log.isDebugEnabled())
283                        log.debug(sm.getString("jaasRealm.beginLogin", principalName, appName));
284    
285                    // What if the LoginModule is in the container class loader ?
286                    ClassLoader ocl = null;
287    
288                    if (isUseContextClassLoader()) {
289                        ocl = Thread.currentThread().getContextClassLoader();
290                        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
291                    }
292    
293                    try {
294                        loginContext = ContextManager.login(appName, callbackHandler);
295                    } catch (AccountExpiredException e) {
296                        if (log.isDebugEnabled())
297                            log.debug(sm.getString("jaasRealm.accountExpired", principalName));
298                        return (null);
299                    } catch (CredentialExpiredException e) {
300                        if (log.isDebugEnabled())
301                            log.debug(sm.getString("jaasRealm.credentialExpired", principalName));
302                        return (null);
303                    } catch (FailedLoginException e) {
304                        if (log.isDebugEnabled())
305                            log.debug(sm.getString("jaasRealm.failedLogin", principalName));
306                        return (null);
307                    } catch (LoginException e) {
308                        log.warn(sm.getString("jaasRealm.loginException", principalName), e);
309                        return (null);
310                    } catch (Throwable e) {
311                        log.error(sm.getString("jaasRealm.unexpectedError"), e);
312                        return (null);
313                    } finally {
314                        if (isUseContextClassLoader()) {
315                            Thread.currentThread().setContextClassLoader(ocl);
316                        }
317                    }
318    
319                    if (log.isDebugEnabled())
320                        log.debug("Login context created " + principalName);
321    
322                    // Negotiate a login via this LoginContext
323                    Subject subject = loginContext.getSubject();
324                    ContextManager.setCallers(subject, subject);
325    
326                    if (log.isDebugEnabled())
327                        log.debug(sm.getString("jaasRealm.loginContextCreated", principalName));
328    
329                    // Return the appropriate Principal for this authenticated Subject
330                    JAASTomcatPrincipal jaasPrincipal = new JAASTomcatPrincipal(principalName);
331                    jaasPrincipal.setSubject(subject);
332    
333                    return (jaasPrincipal);
334                } else {
335                    if (log.isDebugEnabled())
336                        log.debug("Login Failed - null userID");
337                    return null;
338                }
339    
340            } catch (Throwable t) {
341                log.error("error ", t);
342                return null;
343            }
344        }
345    
346        /**
347         * Prepare for active use of the public methods of this <code>Component</code>.
348         *
349         * @throws org.apache.catalina.LifecycleException
350         *          if this component detects a fatal error
351         *          that prevents it from being started
352         */
353        public void start() throws LifecycleException {
354    
355            // Perform normal superclass initialization
356            super.start();
357            setUseContextClassLoader(false);
358        }
359    
360        /**
361         * Gracefully shut down active use of the public methods of this <code>Component</code>.
362         *
363         * @throws LifecycleException if this component detects a fatal error
364         *                            that needs to be reported
365         */
366        public void stop() throws LifecycleException {
367    
368            // Perform normal superclass finalization
369            super.stop();
370    
371        }
372    }