001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.web.tomcat.security;
023:
024: import java.security.Principal;
025: import java.security.cert.X509Certificate;
026: import java.util.ArrayList;
027: import java.util.Iterator;
028: import java.util.Set;
029: import java.util.HashSet;
030: import java.io.IOException;
031: import javax.naming.Context;
032: import javax.naming.InitialContext;
033: import javax.naming.NamingException;
034: import javax.security.auth.Subject;
035: import javax.servlet.http.HttpServletResponse;
036:
037: import org.apache.catalina.LifecycleException;
038: import org.apache.catalina.Realm;
039: import org.apache.catalina.deploy.SecurityConstraint;
040: import org.apache.catalina.deploy.LoginConfig;
041: import org.apache.catalina.connector.Request;
042: import org.apache.catalina.connector.Response;
043: import org.apache.catalina.realm.RealmBase;
044: import org.apache.catalina.realm.Constants;
045: import org.apache.catalina.realm.GenericPrincipal;
046: import org.jboss.logging.Logger;
047: import org.jboss.security.CertificatePrincipal;
048: import org.jboss.security.RealmMapping;
049: import org.jboss.security.SimplePrincipal;
050: import org.jboss.security.SubjectSecurityManager;
051: import org.jboss.security.auth.certs.SubjectDNMapping;
052: import org.jboss.security.auth.callback.CallbackHandlerPolicyContextHandler;
053:
054: /**
055: * An implementation of the catelinz Realm and Valve interfaces. The Realm
056: * implementation handles authentication and authorization using the JBossSX
057: * security framework. It relieas on the JNDI ENC namespace setup by the
058: * AbstractWebContainer. In particular, it uses the java:comp/env/security
059: * subcontext to access the security manager interfaces for authorization and
060: * authenticaton. <p/> The Valve interface is used to associated the
061: * authenticated user with the SecurityAssociation class when a request begins
062: * so that web components may call EJBs and have the principal propagated. The
063: * security association is removed when the request completes.
064: *
065: * @author Scott.Stark@jboss.org
066: * @version $Revision: 57206 $
067: * @see org.jboss.security.AuthenticationManager
068: * @see org.jboss.security.CertificatePrincipal
069: * @see org.jboss.security.RealmMapping
070: * @see org.jboss.security.SimplePrincipal
071: * @see org.jboss.security.SecurityAssociation
072: * @see org.jboss.security.SubjectSecurityManager
073: */
074: public class JBossSecurityMgrRealm extends RealmBase implements Realm {
075: static Logger log = Logger.getLogger(JBossSecurityMgrRealm.class);
076: /**
077: * The converter from X509 cert chain to Princpal
078: */
079: private CertificatePrincipal certMapping = new SubjectDNMapping();
080: /**
081: * The JBossSecurityMgrRealm category trace flag
082: */
083: private boolean trace;
084: /** The mode for handling the all roles mode of role-name=* */
085: private AllRolesMode allRolesMode = AllRolesMode.AUTH_ONLY_MODE;
086:
087: /**
088: * Set the class name of the CertificatePrincipal used for mapping X509 cert
089: * chains to a Princpal.
090: *
091: * @param className the CertificatePrincipal implementation class that must
092: * have a no-arg ctor.
093: * @see org.jboss.security.CertificatePrincipal
094: */
095: public void setCertificatePrincipal(String className) {
096: try {
097: ClassLoader loader = Thread.currentThread()
098: .getContextClassLoader();
099: Class cpClass = loader.loadClass(className);
100: certMapping = (CertificatePrincipal) cpClass.newInstance();
101: } catch (Exception e) {
102: log.error("Failed to load CertificatePrincipal: "
103: + className, e);
104: certMapping = new SubjectDNMapping();
105: }
106: }
107:
108: private Context getSecurityContext() {
109: Context securityCtx = null;
110: // Get the JBoss security manager from the ENC context
111: try {
112: InitialContext iniCtx = new InitialContext();
113: securityCtx = (Context) iniCtx
114: .lookup("java:comp/env/security");
115: } catch (NamingException e) {
116: // Apparently there is no security context?
117: }
118: return securityCtx;
119: }
120:
121: /**
122: * Override to allow a single realm to be shared as a realm and valve
123: */
124: public void start() throws LifecycleException {
125: if (super .started == true) {
126: return;
127: }
128: super .start();
129: trace = log.isTraceEnabled();
130: }
131:
132: /**
133: * Override to allow a single realm to be shared as a realm and valve
134: */
135: public void stop() throws LifecycleException {
136: if (super .started == false) {
137: return;
138: }
139: super .stop();
140: }
141:
142: public boolean hasResourcePermission(Request request,
143: Response response, SecurityConstraint[] constraints,
144: org.apache.catalina.Context context) throws IOException {
145: if (constraints == null || constraints.length == 0) {
146: return (true);
147: }
148:
149: boolean hasPermission = false;
150: // Specifically allow access to the form login and form error pages
151: // and the "j_security_check" action
152: LoginConfig config = context.getLoginConfig();
153: if ((config != null)
154: && (Constants.FORM_METHOD
155: .equals(config.getAuthMethod()))) {
156: String requestURI = request.getRequestPathMB().toString();
157: String loginPage = config.getLoginPage();
158: if (loginPage.equals(requestURI)) {
159: if (trace)
160: log
161: .trace("Allow access to login page "
162: + loginPage);
163: return (true);
164: }
165: String errorPage = config.getErrorPage();
166: if (errorPage.equals(requestURI)) {
167: if (trace)
168: log
169: .trace("Allow access to error page "
170: + errorPage);
171: return (true);
172: }
173: if (requestURI.endsWith(Constants.FORM_ACTION)) {
174: if (trace)
175: log
176: .trace("Allow access to username/password submission");
177: return (true);
178: }
179: }
180:
181: // Which user principal have we already authenticated?
182: Principal principal = request.getPrincipal();
183: boolean denyfromall = false;
184: for (int i = 0; i < constraints.length; i++) {
185: SecurityConstraint constraint = constraints[i];
186:
187: String roles[];
188: if (constraint.getAllRoles()) {
189: // * means all roles defined in web.xml
190: roles = request.getContext().findSecurityRoles();
191: } else {
192: roles = constraint.findAuthRoles();
193: }
194:
195: if (roles == null) {
196: roles = new String[0];
197: }
198:
199: if (trace)
200: log.trace("Checking roles " + principal);
201:
202: if (roles.length == 0 && !constraint.getAllRoles()) {
203: if (constraint.getAuthConstraint()) {
204: if (trace)
205: log.trace("No roles");
206: hasPermission = false; // No listed roles means no access at all
207: denyfromall = true;
208: } else {
209: if (trace)
210: log.trace("Passing all access");
211: return (true);
212: }
213: } else if (principal == null) {
214: if (trace)
215: log
216: .trace("No user authenticated, cannot grant access");
217: hasPermission = false;
218: } else if (!denyfromall) {
219: for (int j = 0; j < roles.length; j++) {
220: if (hasRole(principal, roles[j])) {
221: hasPermission = true;
222: }
223: if (trace)
224: log.trace("No role found: " + roles[j]);
225: }
226: }
227: }
228:
229: if (allRolesMode != AllRolesMode.STRICT_MODE
230: && hasPermission == false && principal != null) {
231: if (trace) {
232: log.trace("Checking for all roles mode: "
233: + allRolesMode);
234: }
235: // Check for an all roles(role-name="*")
236: for (int i = 0; i < constraints.length; i++) {
237: SecurityConstraint constraint = constraints[i];
238: String roles[];
239: // If the all roles mode exists, sets
240: if (constraint.getAllRoles()) {
241: if (allRolesMode == AllRolesMode.AUTH_ONLY_MODE) {
242: if (trace) {
243: log
244: .trace("Granting access for role-name=*, auth-only");
245: }
246: hasPermission = true;
247: break;
248: }
249:
250: // For AllRolesMode.STRICT_AUTH_ONLY_MODE there must be zero roles
251: roles = request.getContext().findSecurityRoles();
252: if (roles.length == 0
253: && allRolesMode == AllRolesMode.STRICT_AUTH_ONLY_MODE) {
254: if (trace) {
255: log
256: .trace("Granting access for role-name=*, strict auth-only");
257: }
258: hasPermission = true;
259: break;
260: }
261: }
262: }
263: }
264:
265: // Return a "Forbidden" message denying access to this resource
266: if (!hasPermission) {
267: response.sendError(HttpServletResponse.SC_FORBIDDEN, sm
268: .getString("realmBase.forbidden"));
269: }
270: return hasPermission;
271: }
272:
273: /**
274: * Return the Principal associated with the specified chain of X509 client
275: * certificates. If there is none, return <code>null</code>.
276: *
277: * @param certs Array of client certificates, with the first one in the array
278: * being the certificate of the client itself.
279: */
280: public Principal authenticate(X509Certificate[] certs) {
281: Principal principal = null;
282: Context securityCtx = getSecurityContext();
283: if (securityCtx == null) {
284: if (trace) {
285: log
286: .trace("No security context for authenticate(X509Certificate[])");
287: }
288: return null;
289: }
290:
291: try {
292: // Get the JBoss security manager from the ENC context
293: SubjectSecurityManager securityMgr = (SubjectSecurityManager) securityCtx
294: .lookup("securityMgr");
295: Subject subject = new Subject();
296: principal = certMapping.toPrinicipal(certs);
297: if (securityMgr.isValid(principal, certs, subject)) {
298: if (trace) {
299: log.trace("User: " + principal
300: + " is authenticated");
301: }
302: SecurityAssociationActions.setPrincipalInfo(principal,
303: certs, subject);
304: // Get the CallerPrincipal mapping
305: RealmMapping realmMapping = (RealmMapping) securityCtx
306: .lookup("realmMapping");
307: Principal oldPrincipal = principal;
308: principal = realmMapping.getPrincipal(oldPrincipal);
309: if (trace) {
310: log.trace("Mapped from input principal: "
311: + oldPrincipal + "to: " + principal);
312: }
313: // Get the caching principal
314: principal = getCachingPrincpal(realmMapping,
315: oldPrincipal, principal, certs, subject);
316: } else {
317: if (trace) {
318: log.trace("User: " + principal
319: + " is NOT authenticated");
320: }
321: principal = null;
322: }
323: } catch (NamingException e) {
324: log.error("Error during authenticate", e);
325: }
326: return principal;
327: }
328:
329: /**
330: * Return the Principal associated with the specified username, which matches
331: * the digest calculated using the given parameters using the method
332: * described in RFC 2069; otherwise return <code>null</code>.
333: *
334: * @param username Username of the Principal to look up
335: * @param digest Digest which has been submitted by the client
336: * @param nonce Unique (or supposedly unique) token which has been used for
337: * this request
338: * @param nc client nonce reuse count
339: * @param cnonce client token
340: * @param qop quality of protection
341: * @param realm Realm name
342: * @param md5a2 Second MD5 digest used to calculate the digest : MD5(Method +
343: * ":" + uri)
344: */
345: public Principal authenticate(String username, String digest,
346: String nonce, String nc, String cnonce, String qop,
347: String realm, String md5a2) {
348: Principal principal = null;
349: Context securityCtx = getSecurityContext();
350: if (securityCtx == null) {
351: if (trace) {
352: log
353: .trace("No security context for authenticate(String, String)");
354: }
355: return null;
356: }
357:
358: Principal caller = (Principal) SecurityAssociationValve.userPrincipal
359: .get();
360: if (caller == null && username == null && digest == null) {
361: return null;
362: }
363:
364: try {
365: DigestCallbackHandler handler = new DigestCallbackHandler(
366: username, nonce, nc, cnonce, qop, realm, md5a2);
367: CallbackHandlerPolicyContextHandler
368: .setCallbackHandler(handler);
369:
370: // Get the JBoss security manager from the ENC context
371: SubjectSecurityManager securityMgr = (SubjectSecurityManager) securityCtx
372: .lookup("securityMgr");
373: principal = new SimplePrincipal(username);
374: Subject subject = new Subject();
375: if (securityMgr.isValid(principal, digest, subject)) {
376: log.trace("User: " + username + " is authenticated");
377: SecurityAssociationActions.setPrincipalInfo(principal,
378: digest, subject);
379: // Get the CallerPrincipal mapping
380: RealmMapping realmMapping = (RealmMapping) securityCtx
381: .lookup("realmMapping");
382: Principal oldPrincipal = principal;
383: principal = realmMapping.getPrincipal(oldPrincipal);
384: if (trace) {
385: log.trace("Mapped from input principal: "
386: + oldPrincipal + "to: " + principal);
387: }
388: // Get the caching principal
389: principal = getCachingPrincpal(realmMapping,
390: oldPrincipal, principal, digest, subject);
391: } else {
392: principal = null;
393: if (trace) {
394: log.trace("User: " + username
395: + " is NOT authenticated");
396: }
397: }
398: } catch (NamingException e) {
399: principal = null;
400: log.error("Error during authenticate", e);
401: } finally {
402: CallbackHandlerPolicyContextHandler
403: .setCallbackHandler(null);
404: }
405: if (trace) {
406: log.trace("End authenticate, principal=" + principal);
407: }
408: return principal;
409: }
410:
411: /**
412: * Return the Principal associated with the specified username and
413: * credentials, if there is one; otherwise return <code>null</code>.
414: *
415: * @param username Username of the Principal to look up
416: * @param credentials Password or other credentials to use in authenticating
417: * this username
418: */
419: public Principal authenticate(String username, String credentials) {
420: if (trace) {
421: log.trace("Begin authenticate, username=" + username);
422: }
423: Principal principal = null;
424: Context securityCtx = getSecurityContext();
425: if (securityCtx == null) {
426: if (trace) {
427: log
428: .trace("No security context for authenticate(String, String)");
429: }
430: return null;
431: }
432:
433: Principal caller = (Principal) SecurityAssociationValve.userPrincipal
434: .get();
435: if (caller == null && username == null && credentials == null) {
436: return null;
437: }
438:
439: try {
440: // Get the JBoss security manager from the ENC context
441: SubjectSecurityManager securityMgr = (SubjectSecurityManager) securityCtx
442: .lookup("securityMgr");
443: principal = new SimplePrincipal(username);
444: Subject subject = new Subject();
445: if (securityMgr.isValid(principal, credentials, subject)) {
446: log.trace("User: " + username + " is authenticated");
447: SecurityAssociationActions.setPrincipalInfo(principal,
448: credentials, subject);
449: // Get the CallerPrincipal mapping
450: RealmMapping realmMapping = (RealmMapping) securityCtx
451: .lookup("realmMapping");
452: Principal oldPrincipal = principal;
453: principal = realmMapping.getPrincipal(oldPrincipal);
454: if (trace) {
455: log.trace("Mapped from input principal: "
456: + oldPrincipal + "to: " + principal);
457: }
458: // Get the caching principal
459: principal = getCachingPrincpal(realmMapping,
460: oldPrincipal, principal, credentials, subject);
461: } else {
462: principal = null;
463: if (trace) {
464: log.trace("User: " + username
465: + " is NOT authenticated");
466: }
467: }
468: } catch (NamingException e) {
469: principal = null;
470: log.error("Error during authenticate", e);
471: }
472: if (trace) {
473: log.trace("End authenticate, principal=" + principal);
474: }
475: return principal;
476: }
477:
478: /**
479: * Returns <code>true</code> if the specified user <code>Principal</code> has
480: * the specified security role, within the context of this
481: * <code>Realm</code>; otherwise return <code>false</code>. This will be true
482: * when an associated role <code>Principal</code> can be found whose
483: * <code>getName</code> method returns a <code>String</code> equalling the
484: * specified role.
485: *
486: * @param principal <code>Principal</code> for whom the role is to be
487: * checked
488: * @param role Security role to be checked
489: */
490: public boolean hasRole(Principal principal, String role) {
491: return super .hasRole(principal, role);
492: }
493:
494: /**
495: * Return the Principal associated with the specified username and
496: * credentials, if there is one; otherwise return <code>null</code>.
497: *
498: * @param username Username of the Principal to look up
499: * @param credentials Password or other credentials to use in authenticating
500: * this username
501: */
502: public Principal authenticate(String username, byte[] credentials) {
503: return authenticate(username, new String(credentials));
504: }
505:
506: /**
507: * Return a short name for this Realm implementation, for use in log
508: * messages.
509: */
510: protected String getName() {
511: return getClass().getName();
512: }
513:
514: /**
515: * Return the password associated with the given principal's user name.
516: */
517: protected String getPassword(String username) {
518: String password = null;
519: return password;
520: }
521:
522: /**
523: * Return the Principal associated with the given user name.
524: */
525: protected Principal getPrincipal(String username) {
526: return new SimplePrincipal(username);
527: }
528:
529: /**
530: * Access the set of role Princpals associated with the given caller princpal.
531: *
532: * @param principal - the Principal mapped from the authentication principal
533: * and visible from the HttpServletRequest.getUserPrincipal
534: * @return a possible null Set<Principal> for the caller roles
535: */
536: protected Set getPrincipalRoles(Principal principal) {
537: if ((principal instanceof GenericPrincipal) == false)
538: throw new IllegalStateException(
539: "Expected GenericPrincipal, but saw: "
540: + principal.getClass());
541: GenericPrincipal gp = (GenericPrincipal) principal;
542: String[] roleNames = gp.getRoles();
543: Set userRoles = new HashSet();
544: if (roleNames != null) {
545: for (int n = 0; n < roleNames.length; n++) {
546: SimplePrincipal sp = new SimplePrincipal(roleNames[n]);
547: userRoles.add(sp);
548: }
549: }
550: return userRoles;
551: }
552:
553: /**
554: * Create the session principal tomcat will cache to avoid callouts to this
555: * Realm.
556: *
557: * @param realmMapping - the role mapping security manager
558: * @param authPrincipal - the principal used for authentication and stored in
559: * the security manager cache
560: * @param callerPrincipal - the possibly different caller principal
561: * representation of the authenticated principal
562: * @param credential - the credential used for authentication
563: * @return the tomcat session principal wrapper
564: */
565: protected Principal getCachingPrincpal(RealmMapping realmMapping,
566: Principal authPrincipal, Principal callerPrincipal,
567: Object credential, Subject subject) {
568: // Cache the user roles in the principal
569: Set userRoles = realmMapping.getUserRoles(authPrincipal);
570: ArrayList roles = new ArrayList();
571: if (userRoles != null) {
572: Iterator iterator = userRoles.iterator();
573: while (iterator.hasNext()) {
574: Principal role = (Principal) iterator.next();
575: roles.add(role.getName());
576: }
577: }
578: JBossGenericPrincipal gp = new JBossGenericPrincipal(this,
579: subject, authPrincipal, callerPrincipal, credential,
580: roles, userRoles);
581: return gp;
582: }
583: }
|