Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthorizationManager.java URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthorizationManager.java?rev=627255&view=auto ============================================================================== --- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthorizationManager.java (added) +++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/AuthorizationManager.java Tue Feb 12 21:53:55 2008 @@ -0,0 +1,684 @@ +/* + JSPWiki - a JSP-based WikiWiki clone. + + Copyright (C) 2001-2003 Janne Jalkanen ([EMAIL PROTECTED]) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.ecyrd.jspwiki.auth; + + +import java.io.File; +import java.net.URL; +import java.security.*; +import java.security.cert.Certificate; +import java.util.Map; +import java.util.Properties; +import java.util.WeakHashMap; + +import org.apache.log4j.Logger; +import org.freshcookies.security.policy.LocalPolicy; +import org.freshcookies.security.policy.PolicyException; + +import com.ecyrd.jspwiki.NoRequiredPropertyException; +import com.ecyrd.jspwiki.WikiEngine; +import com.ecyrd.jspwiki.WikiException; +import com.ecyrd.jspwiki.WikiPage; +import com.ecyrd.jspwiki.WikiSession; +import com.ecyrd.jspwiki.auth.acl.Acl; +import com.ecyrd.jspwiki.auth.acl.AclEntry; +import com.ecyrd.jspwiki.auth.acl.UnresolvedPrincipal; +import com.ecyrd.jspwiki.auth.authorize.Role; +import com.ecyrd.jspwiki.auth.permissions.AllPermission; +import com.ecyrd.jspwiki.auth.permissions.PagePermission; +import com.ecyrd.jspwiki.auth.user.UserDatabase; +import com.ecyrd.jspwiki.auth.user.UserProfile; +import com.ecyrd.jspwiki.event.WikiEventListener; +import com.ecyrd.jspwiki.event.WikiEventManager; +import com.ecyrd.jspwiki.event.WikiSecurityEvent; +import com.ecyrd.jspwiki.util.ClassUtil; + +/** + * <p>Manages all access control and authorization; determines what authenticated + * users are allowed to do.</p> + * <p>Privileges in JSPWiki are expressed as Java-standard [EMAIL PROTECTED] java.security.Permission} + * classes. There are two types of permissions:</p> + * <ul> + * <li>[EMAIL PROTECTED] com.ecyrd.jspwiki.auth.permissions.WikiPermission} - privileges that apply + * to an entire wiki instance: <em>e.g.,</em> editing user profiles, creating pages, creating groups</li> + * <li>[EMAIL PROTECTED] com.ecyrd.jspwiki.auth.permissions.PagePermission} - privileges that apply + * to a single wiki page or range of pages: <em>e.g.,</em> reading, editing, renaming + * </ul> + * <p>Calling classes determine whether they are entitled to perform a particular action + * by constructing the appropriate permission first, then passing it and the current + * [EMAIL PROTECTED] com.ecyrd.jspwiki.WikiSession} to the + * [EMAIL PROTECTED] #checkPermission(WikiSession, Permission)} method. If the session's + * Subject possesses the permission, the action is allowed.</p> + * <p>For WikiPermissions, the decision criteria is relatively simple: the caller either + * possesses the permission, as granted by the wiki security policy -- or not.</p> + * <p>For PagePermissions, the logic is exactly the same if the page being checked + * does not have an access control list. However, if the page does have an ACL, the + * authorization decision is made based the <em>union</em> of the permissions + * granted in the ACL and in the security policy. In other words, the user must + * be named in the ACL (or belong to a group or role that is named in the ACL) + * <em>and</em> be granted (at least) the same permission in the security policy. We + * do this to prevent a user from gaining more permissions than they already + * have, based on the security policy.</p> + * <p>See the [EMAIL PROTECTED] #checkPermission(WikiSession, Permission)} and + * [EMAIL PROTECTED] #hasRoleOrPrincipal(WikiSession, Principal)} methods for more information + * on the authorization logic.</p> + * @author Andrew Jaquith + * @since 2.3 + * @see AuthenticationManager + */ +public final class AuthorizationManager +{ + private static final Logger log = Logger.getLogger( AuthorizationManager.class ); + /** + * The default external Authorizer is the [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer} + */ + public static final String DEFAULT_AUTHORIZER = "com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer"; + + /** Name of the default security policy file, in WEB-INF. */ + protected static final String DEFAULT_POLICY = "jspwiki.policy"; + + /** + * The property name in jspwiki.properties for specifying the external [EMAIL PROTECTED] Authorizer}. + */ + public static final String PROP_AUTHORIZER = "jspwiki.authorizer"; + + private Authorizer m_authorizer = null; + + /** Cache for storing ProtectionDomains used to evaluate the local policy. */ + private Map m_cachedPds = new WeakHashMap(); + + private WikiEngine m_engine = null; + + private LocalPolicy m_localPolicy = null; + + private boolean m_useJAAS = true; + + /** + * Constructs a new AuthorizationManager instance. + */ + public AuthorizationManager() + { + } + + /** + * Returns <code>true</code> or <code>false</code>, depending on + * whether a Permission is allowed for the Subject associated with + * a supplied WikiSession. The access control algorithm works this way: + * <ol> + * <li>The [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.acl.Acl} for the page is obtained</li> + * <li>The Subject associated with the current + * [EMAIL PROTECTED] com.ecyrd.jspwiki.WikiSession} is obtained</li> + * <li>If the Subject's Principal set includes the Role Principal that is + * the administrator group, always allow the Permission</li> + * <li>For all permissions, check to see if the Permission is allowed according + * to the default security policy. If it isn't, deny the permission and halt + * further processing.</li> + * <li>If there is an Acl, get the list of Principals assigned this + * Permission in the Acl: these will be role, group or user Principals, or + * [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.acl.UnresolvedPrincipal}s (see below). + * Then iterate through the Subject's Principal set and determine whether + * the user (Subject) posesses any one of these specified Roles or + * Principals. The matching process delegates to + * [EMAIL PROTECTED] #hasRoleOrPrincipal(WikiSession, Principal)}. + * </ol> + * <p> + * Note that when iterating through the Acl's list of authorized Principals, + * it is possible that one or more of the Acl's Principal entries are of + * type <code>UnresolvedPrincipal</code>. This means that the last time + * the ACL was read, the Principal (user, built-in Role, authorizer Role, or + * wiki Group) could not be resolved: the Role was not valid, the user + * wasn't found in the UserDatabase, or the Group wasn't known to (e.g., + * cached) in the GroupManager. If an <code>UnresolvedPrincipal</code> is + * encountered, this method will attempt to resolve it first <em>before</em> + * checking to see if the Subject possesses this principal, by calling + * [EMAIL PROTECTED] #resolvePrincipal(String)}. If the (re-)resolution does not + * succeed, the access check for the principal will fail by definition (the + * Subject should never contain UnresolvedPrincipals). + * </p> + * <p> + * If security not set to JAAS, will return true. + * </p> + * @param session the current wiki session + * @param permission the Permission being checked + * @see #hasRoleOrPrincipal(WikiSession, Principal) + * @return the result of the Permission check + */ + public final boolean checkPermission( WikiSession session, Permission permission ) + { + if( !m_useJAAS ) + { + // + // Nobody can login, if JAAS is turned off. + // + + if( permission == null || "login".equals( permission.getActions() ) ) + return false; + + return true; + } + + // + // A slight sanity check. + // + if ( session == null || permission == null ) + { + fireEvent( WikiSecurityEvent.ACCESS_DENIED, null, permission ); + return false; + } + + Principal user = session.getLoginPrincipal(); + + // Always allow the action if user has AllPermission + Permission allPermission = new AllPermission( m_engine.getApplicationName() ); + boolean hasAllPermission = checkStaticPermission( session, allPermission ); + if ( hasAllPermission ) + { + fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission ); + return true; + } + + // If the user doesn't have *at least* the permission + // granted by policy, return false. + boolean hasPolicyPermission = checkStaticPermission( session, permission ); + if ( !hasPolicyPermission ) + { + fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission ); + return false; + } + + // If this isn't a PagePermission, it's allowed + if ( ! ( permission instanceof PagePermission ) ) + { + fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission ); + return true; + } + + // + // If the page or ACL is null, it's allowed. + // + String pageName = ((PagePermission)permission).getPage(); + WikiPage page = m_engine.getPage( pageName ); + Acl acl = ( page == null) ? null : m_engine.getAclManager().getPermissions( page ); + if ( page == null || acl == null || acl.isEmpty() ) + { + fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission ); + return true; + } + + // + // Next, iterate through the Principal objects assigned + // this permission. If the context's subject possesses + // any of these, the action is allowed. + + Principal[] aclPrincipals = acl.findPrincipals( permission ); + + log.debug( "Checking ACL entries..." ); + log.debug( "Acl for this page is: " + acl ); + log.debug( "Checking for principal: " + String.valueOf(aclPrincipals) ); + log.debug( "Permission: " + permission ); + + for( int i = 0; i < aclPrincipals.length; i++ ) + { + Principal aclPrincipal = aclPrincipals[i]; + + // If the ACL principal we're looking at is unresolved, + // try to resolve it here & correct the Acl + if ( aclPrincipal instanceof UnresolvedPrincipal ) + { + AclEntry aclEntry = acl.getEntry( aclPrincipal ); + aclPrincipal = resolvePrincipal( aclPrincipal.getName() ); + if ( aclEntry != null && !( aclPrincipal instanceof UnresolvedPrincipal ) ) + { + aclEntry.setPrincipal( aclPrincipal ); + } + } + + if ( hasRoleOrPrincipal( session, aclPrincipal ) ) + { + fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission ); + return true; + } + } + fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission ); + return false; + } + + /** + * <p>Determines if the Subject associated with a + * supplied WikiSession contains a desired Role or GroupPrincipal. + * The algorithm simply checks to see if the Subject possesses + * the Role or GroupPrincipal it in its Principal set. Note that + * any user (anyonymous, asserted, authenticated) can possess + * a built-in role. But a user <em>must</em> be authenticated to + * possess a role other than one of the built-in ones. + * We do this to prevent privilege escalation.</p> + * <p>For all other cases, this method returns <code>false</code>.</p> + * <p>Note that this method does <em>not</em> consult the external + * Authorizer or GroupManager; it relies on the Principals that + * have been injected into the user's Subject at login time, or + * after group creation/modification/deletion.</p> + * @param session the current wiki session, which must be non-null. If null, + * the result of this method always returns <code>false</code> + * @param principal the Principal (role or group principal) to look + * for, which must be non-<cor>null</code>. If <code>null</code>, + * the result of this method always returns <code>false</code> + * @return <code>true</code> if the Subject supplied with the WikiContext + * posesses the Role or GroupPrincipal, <code>false</code> otherwise + */ + public final boolean isUserInRole( WikiSession session, Principal principal ) + { + if ( session == null || principal == null || + AuthenticationManager.isUserPrincipal( principal ) ) + { + return false; + } + + // Any type of user can possess a built-in role + if ( principal instanceof Role && Role.isBuiltInRole( (Role)principal ) ) + { + return session.hasPrincipal( principal ); + } + + // Only authenticated users can posssess groups or custom roles + if ( session.isAuthenticated() && AuthenticationManager.isRolePrincipal( principal ) ) + { + return session.hasPrincipal( principal ); + } + return false; + } + + /** + * Returns the current external [EMAIL PROTECTED] Authorizer} in use. This method + * is guaranteed to return a properly-initialized Authorizer, unless + * it could not be initialized. In that case, this method throws + * a [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.WikiSecurityException}. + * @throws com.ecyrd.jspwiki.auth.WikiSecurityException if the Authorizer could + * not be initialized + * @return the current Authorizer + */ + public final Authorizer getAuthorizer() throws WikiSecurityException + { + if ( m_authorizer != null ) + { + return m_authorizer; + } + throw new WikiSecurityException( "Authorizer did not initialize properly. Check the logs." ); + } + + /** + * <p>Determines if the Subject associated with a supplied WikiSession contains + * a desired user Principal or built-in Role principal, OR is a member a + * Group or external Role. The rules are as follows:</p> + * <ol> + * <li>First, if desired Principal is a Role or GroupPrincipal, delegate to + * [EMAIL PROTECTED] #isUserInRole(WikiSession, Principal)} and + * return the result.</li> + * <li>Otherwise, we're looking for a user Principal, + * so iterate through the Principal set and see if + * any share the same name as the one we are looking for.</li> + * </ol> + * <p><em>Note: if the Principal parameter is a user principal, the session + * must be authenticated in order for the user to "possess it". Anonymous + * or asserted sessions will never posseess a named user principal.</em></p> + * @param session the current wiki session, which must be non-null. If null, + * the result of this method always returns <code>false</code> + * @param principal the Principal (role, group, or user principal) to look + * for, which must be non-null. If null, the result of this + * method always returns <code>false</code> + * @return <code>true</code> if the Subject supplied with the WikiContext + * posesses the Role, GroupPrincipal or desired + * user Principal, <code>false</code> otherwise + */ + protected boolean hasRoleOrPrincipal( WikiSession session, Principal principal ) + { + // If either parameter is null, always deny + if( session == null || principal == null ) + { + return false; + } + + // If principal is role, delegate to isUserInRole + if( AuthenticationManager.isRolePrincipal( principal ) ) + { + return isUserInRole( session, principal ); + } + + // We must be looking for a user principal, assuming that the user + // has been properly logged in. + // So just look for a name match. + if( session.isAuthenticated() && AuthenticationManager.isUserPrincipal( principal ) ) + { + String principalName = principal.getName(); + Principal[] userPrincipals = session.getPrincipals(); + for( int i = 0; i < userPrincipals.length; i++ ) + { + Principal userPrincipal = userPrincipals[i]; + if( userPrincipal.getName().equals( principalName ) ) + { + return true; + } + } + } + return false; + } + + /** + * Initializes AuthorizationManager with an engine and set of properties. + * Expects to find property 'jspwiki.authorizer' with a valid Authorizer + * implementation name to take care of group lookup operations. + * @param engine the wiki engine + * @param properties the set of properties used to initialize the wiki engine + * @throws WikiException if the AuthorizationManager cannot be initialized + */ + public final void initialize( WikiEngine engine, Properties properties ) throws WikiException + { + m_engine = engine; + + m_useJAAS = AuthenticationManager.SECURITY_JAAS.equals( properties.getProperty(AuthenticationManager.PROP_SECURITY, AuthenticationManager.SECURITY_JAAS ) ); + + if( !m_useJAAS ) return; + + // + // JAAS authorization continues + // + m_authorizer = getAuthorizerImplementation( properties ); + m_authorizer.initialize( engine, properties ); + + // Initialize local security policy + try + { + URL policyURL = AuthenticationManager.findConfigFile( engine, DEFAULT_POLICY ); + + if (policyURL != null) + { + File policyFile = new File( policyURL.getPath() ); + m_localPolicy = new LocalPolicy( policyFile, engine.getContentEncoding() ); + m_localPolicy.refresh(); + log.info("Initialized default security policy: " + policyFile.getAbsolutePath()); + } + else + { + StringBuffer sb = new StringBuffer("JSPWiki was unable to initialize the "); + sb.append("default security policy (WEB-INF/jspwiki.policy) file. "); + sb.append("Please ensure that the jspwiki.policy file exists in the default location. "); + sb.append("This file should exist regardless of the existance of a global policy file. "); + sb.append("The global policy file is identified by the java.security.policy variable. "); + WikiSecurityException wse = new WikiSecurityException(sb.toString()); + log.fatal(sb.toString(), wse); + throw wse; + } + } + catch ( PolicyException e ) + { + log.error("Could not initialize local security policy: " + e.getMessage() ); + throw new WikiException( e.getMessage() ); + } + } + + /** + * Returns <code>true</code> if JSPWiki's JAAS authorization system + * is used for authorization in addition to container controls. + * @return the result + */ + protected boolean isJAASAuthorized() + { + return m_useJAAS; + } + + /** + * Attempts to locate and initialize a Authorizer to use with this manager. + * Throws a WikiException if no entry is found, or if one fails to + * initialize. + * @param props jspwiki.properties, containing a + * 'jspwiki.authorization.provider' class name + * @return a Authorizer used to get page authorization information + * @throws WikiException + */ + private final Authorizer getAuthorizerImplementation( Properties props ) throws WikiException + { + String authClassName = props.getProperty( PROP_AUTHORIZER, DEFAULT_AUTHORIZER ); + return (Authorizer) locateImplementation( authClassName ); + } + + private final Object locateImplementation( String clazz ) throws WikiException + { + if ( clazz != null ) + { + try + { + Class authClass = ClassUtil.findClass( "com.ecyrd.jspwiki.auth.authorize", clazz ); + Object impl = authClass.newInstance(); + return impl; + } + catch( ClassNotFoundException e ) + { + log.fatal( "Authorizer " + clazz + " cannot be found", e ); + throw new WikiException( "Authorizer " + clazz + " cannot be found" ); + } + catch( InstantiationException e ) + { + log.fatal( "Authorizer " + clazz + " cannot be created", e ); + throw new WikiException( "Authorizer " + clazz + " cannot be created" ); + } + catch( IllegalAccessException e ) + { + log.fatal( "You are not allowed to access this authorizer class", e ); + throw new WikiException( "You are not allowed to access this authorizer class" ); + } + } + + throw new NoRequiredPropertyException( "Unable to find a " + PROP_AUTHORIZER + " entry in the properties.", + PROP_AUTHORIZER ); + } + + /** + * Checks to see if the local security policy allows a particular static Permission. + * Do not use this method for normal permission checks; use + * [EMAIL PROTECTED] #checkPermission(WikiSession, Permission)} instead. + * @param principals the Principals to check + * @param permission the Permission + * @return the result + */ + protected boolean allowedByLocalPolicy( Principal[] principals, Permission permission ) + { + for ( int i = 0; i < principals.length; i++ ) + { + // Get ProtectionDomain for this Principal from cache, or create new one + ProtectionDomain pd = (ProtectionDomain)m_cachedPds.get( principals[i] ); + if ( pd == null ) + { + ClassLoader cl = this.getClass().getClassLoader(); + CodeSource cs = new CodeSource( null, (Certificate[])null ); + pd = new ProtectionDomain( cs, null, cl, new Principal[]{ principals[i] } ); + m_cachedPds.put( principals[i], pd ); + } + + // Consult the local policy and get the answer + if ( m_localPolicy.implies( pd, permission ) ) + { + return true; + } + } + return false; + } + + /** + * Determines whether a Subject posesses a given "static" Permission as + * defined in the security policy file. This method uses standard Java 2 + * security calls to do its work. Note that the current access control + * context's <code>codeBase</code> is effectively <em>this class</em>, + * not that of the caller. Therefore, this method will work best when what + * matters in the policy is <em>who</em> makes the permission check, not + * what the caller's code source is. Internally, this method works by + * excuting <code>Subject.doAsPrivileged</code> with a privileged action + * that simply calls [EMAIL PROTECTED] java.security.AccessController#checkPermission(Permission)}. + * @link AccessController#checkPermission(java.security.Permission). A + * caught exception (or lack thereof) determines whether the privilege + * is absent (or present). + * @param session the WikiSession whose permission status is being queried + * @param permission the Permission the Subject must possess + * @return <code>true</code> if the Subject posesses the permission, + * <code>false</code> otherwise + */ + protected final boolean checkStaticPermission( final WikiSession session, final Permission permission ) + { + if( !m_useJAAS ) return true; + + Boolean allowed = (Boolean)WikiSession.doPrivileged( session, new PrivilegedAction() + { + public Object run() + { + try + { + // Check the JVM-wide security policy first + AccessController.checkPermission( permission ); + return Boolean.TRUE; + } + catch( AccessControlException e ) + { + // Global policy denied the permission + } + + // Try the local policy - check each Role/Group and User Principal + if ( allowedByLocalPolicy( session.getRoles(), permission ) || + allowedByLocalPolicy( session.getPrincipals(), permission ) ) + { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + } ); + return allowed.booleanValue(); + } + + /** + * <p>Given a supplied string representing a Principal's name from an Acl, this + * method resolves the correct type of Principal (role, group, or user). + * This method is guaranteed to always return a Principal. + * The algorithm is straightforward:</p> + * <ol> + * <li>If the name matches one of the built-in [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.authorize.Role} names, + * return that built-in Role</li> + * <li>If the name matches one supplied by the current + * [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.Authorizer}, return that Role</li> + * <li>If the name matches a group managed by the + * current [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.authorize.GroupManager}, return that Group</li> + * <li>Otherwise, assume that the name represents a user + * principal. Using the current [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.user.UserDatabase}, find the + * first user who matches the supplied name by calling + * [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.user.UserDatabase#find(String)}.</li> + * <li>Finally, if a user cannot be found, manufacture + * and return a generic [EMAIL PROTECTED] com.ecyrd.jspwiki.auth.acl.UnresolvedPrincipal}</li> + * </ol> + * @param name the name of the Principal to resolve + * @return the fully-resolved Principal + */ + public final Principal resolvePrincipal( String name ) + { + if( !m_useJAAS ) + { + return new UnresolvedPrincipal(name); + } + + // Check built-in Roles first + Role role = new Role(name); + if ( Role.isBuiltInRole( role ) ) + { + return role; + } + + // Check Authorizer Roles + Principal principal = m_authorizer.findRole( name ); + if ( principal != null ) + { + return principal; + } + + // Check Groups + principal = m_engine.getGroupManager().findRole( name ); + if ( principal != null ) + { + return principal; + } + + // Ok, no luck---this must be a user principal + Principal[] principals = null; + UserProfile profile = null; + UserDatabase db = m_engine.getUserManager().getUserDatabase(); + try + { + profile = db.find( name ); + principals = db.getPrincipals( profile.getLoginName() ); + for (int i = 0; i < principals.length; i++) + { + principal = principals[i]; + if ( principal.getName().equals( name ) ) + { + return principal; + } + } + } + catch( NoSuchPrincipalException e ) + { + // We couldn't find the user... + } + // Ok, no luck---mark this as unresolved and move on + return new UnresolvedPrincipal( name ); + } + + + // events processing ....................................................... + + /** + * Registers a WikiEventListener with this instance. + * @param listener the event listener + */ + public final synchronized void addWikiEventListener( WikiEventListener listener ) + { + WikiEventManager.addWikiEventListener( this, listener ); + } + + /** + * Un-registers a WikiEventListener with this instance. + * @param listener the event listener + */ + public final synchronized void removeWikiEventListener( WikiEventListener listener ) + { + WikiEventManager.removeWikiEventListener( this, listener ); + } + + /** + * Fires a WikiSecurityEvent of the provided type, user, + * and permission to all registered listeners. + * + * @see com.ecyrd.jspwiki.event.WikiSecurityEvent + * @param type the event type to be fired + * @param user the user associated with the event + * @param permission the permission the subject must possess + */ + protected final void fireEvent( int type, Principal user, Object permission ) + { + if ( WikiEventManager.isListening(this) ) + { + WikiEventManager.fireEvent(this,new WikiSecurityEvent(this,type,user,permission)); + } + } + +}
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/Authorizer.java URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/Authorizer.java?rev=627255&view=auto ============================================================================== --- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/Authorizer.java (added) +++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/Authorizer.java Tue Feb 12 21:53:55 2008 @@ -0,0 +1,76 @@ +/* + JSPWiki - a JSP-based WikiWiki clone. + + Copyright (C) 2001-2007 Janne Jalkanen ([EMAIL PROTECTED]) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.ecyrd.jspwiki.auth; + +import java.security.Principal; +import java.util.Properties; + +import com.ecyrd.jspwiki.WikiEngine; +import com.ecyrd.jspwiki.WikiSession; + +/** + * Interface for service providers of authorization information. + * @author Andrew Jaquith + * @since 2.3 + */ +public interface Authorizer +{ + + /** + * Returns an array of role Principals this Authorizer knows about. + * This method will always return an array; an implementing class may + * choose to return an zero-length array if it has no ability to identify + * the roles under its control. + * @return an array of Principals representing the roles + */ + public Principal[] getRoles(); + + /** + * Looks up and returns a role Principal matching a given String. + * If a matching role cannot be found, this method returns <code>null</code>. + * Note that it may not always be feasible for an Authorizer + * implementation to return a role Principal. + * @param role the name of the role to retrieve + * @return the role Principal + */ + public Principal findRole( String role ); + + /** + * Initializes the authorizer. + * @param engine the current wiki engine + * @param props the wiki engine initialization properties + * @throws WikiSecurityException if the Authorizer could not be initialized + */ + public void initialize( WikiEngine engine, Properties props ) throws WikiSecurityException; + + /** + * Determines whether the Subject associated with a WikiSession is in a + * particular role. This method takes two parameters: the WikiSession + * containing the subject and the desired role ( which may be a Role or a + * Group). If either parameter is <code>null</code>, this method must + * return <code>false</code>. + * @param session the current WikiSession + * @param role the role to check + * @return <code>true</code> if the user is considered to be in the role, + * <code>false</code> otherwise + */ + public boolean isUserInRole( WikiSession session, Principal role ); + +} Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/GroupPrincipal.java URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/GroupPrincipal.java?rev=627255&view=auto ============================================================================== --- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/GroupPrincipal.java (added) +++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/GroupPrincipal.java Tue Feb 12 21:53:55 2008 @@ -0,0 +1,95 @@ +/* + * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 Janne Jalkanen + * ([EMAIL PROTECTED]) This program is free software; you can redistribute + * it and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. This program is distributed + * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. You should have + * received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 59 Temple + * Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.ecyrd.jspwiki.auth; + +import java.security.Principal; + +/** + * Immutable Principal that represents a Group. GroupPrincipals are injected + * into a Subject's principal list at the time of authentication (login), and + * serve as proxies for Group objects for the purposes of making Java 2 security + * policy decisions. We add GroupPrincipals instead of the actual Groups because + * calling classes should never be able to obtain a mutable object (Group + * memberships can be changed by callers). Administrators who wish to grant + * privileges to specific wiki groups via the security policy file should always specify + * principals of type GroupPrincipal. + * @see com.ecyrd.jspwiki.auth.authorize.Group + * @author Andrew Jaquith + * @since 2.3.79 + */ +public final class GroupPrincipal implements Principal +{ + private final String m_name; + + /** + * Constructs a new GroupPrincipal object with a supplied name. + * + * @param group the wiki group; cannot be <code>null</code> + */ + public GroupPrincipal( String group ) + { + if ( group == null ) + { + throw new IllegalArgumentException( "Group parameter cannot be null." ); + } + m_name = group; + } + + /** + * Returns the name of the group principal. + * @return the name + * @see java.security.Principal#getName() + */ + public final String getName() + { + return m_name; + } + + /** + * Two GroupPrincipals are equal if their names are equal. + * @param obj the object to compare + * @return the result of the equality test + * @see java.lang.Object#equals(java.lang.Object) + */ + public final boolean equals( Object obj ) + { + if ( !( obj instanceof GroupPrincipal ) ) + { + return false; + } + GroupPrincipal p = (GroupPrincipal)obj; + return p.m_name.equals( m_name ); + } + + /** + * Returns the hashcode for this object. + * @return the hash code + * @see java.lang.Object#hashCode() + */ + public final int hashCode() + { + return m_name.hashCode(); + } + + /** + * Returns a string representation of this object. + * @return the string + * @see java.lang.Object#toString() + */ + public final String toString() + { + return "[GroupPrincipal " + m_name + "]"; + } + +} Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/NoSuchPrincipalException.java URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/NoSuchPrincipalException.java?rev=627255&view=auto ============================================================================== --- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/NoSuchPrincipalException.java (added) +++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/NoSuchPrincipalException.java Tue Feb 12 21:53:55 2008 @@ -0,0 +1,40 @@ +/* + JSPWiki - a JSP-based WikiWiki clone. + + Copyright (C) 2001-2003 Janne Jalkanen ([EMAIL PROTECTED]) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.ecyrd.jspwiki.auth; + +/** + * Thrown in some error situations where a WikiPrincipal object does not exist. + * @author Andrew Jaquith + * @since 2.3 + */ +public final class NoSuchPrincipalException + extends WikiSecurityException +{ + private static final long serialVersionUID = 3257849895976186169L; + + /** + * Constructs a new exception object with a supplied message. + * @param msg the message + */ + public NoSuchPrincipalException( String msg ) + { + super(msg); + } +} Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PolicyLoader.java URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PolicyLoader.java?rev=627255&view=auto ============================================================================== --- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PolicyLoader.java (added) +++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PolicyLoader.java Tue Feb 12 21:53:55 2008 @@ -0,0 +1,373 @@ +/* + JSPWiki - a JSP-based WikiWiki clone. + + Copyright (C) 2001-2007 Janne Jalkanen ([EMAIL PROTECTED]) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.ecyrd.jspwiki.auth; + +import java.io.File; +import java.net.URL; +import java.security.AccessController; +import java.security.Policy; +import java.security.PrivilegedAction; +import java.security.Security; + +import javax.security.auth.login.Configuration; + +import org.apache.log4j.Logger; + +/** + * <p> + * Initializes JVM configurations for JAAS and Java 2 security policy. Callers + * can use the static methods in this class ([EMAIL PROTECTED] #isJaasConfigured()} + * and [EMAIL PROTECTED] #isSecurityPolicyConfigured()}) to inquire whether a JAAS + * login configuration exists, or whether a custom Java security policy is in + * use. Additional methods allow callers to set the JAAS and security policy + * configurations to supplied URLs ([EMAIL PROTECTED] #setJaasConfiguration(URL)} + * and [EMAIL PROTECTED] #setSecurityPolicy(URL)}). + * </p> + * <p> + * If either the JAAS configuration and security policy are set using methods in + * this class, the resulting configuration or policy is <i>global</i> to the + * JVM. Thus, in a multi-webapp scenario, this means that the first webapp to be + * loaded by the container wins. Thus, for containers hosting multiple wikis, + * the administrator will need to manually configure the + * <code>java.security.policy</code> and + * <code>java.security.auth.login.config properties</code>. In other words, + * multi-wiki deployments will always require manual (one-time) configuration. + * </p> + * <p> + * The security policy-related methods [EMAIL PROTECTED] #isSecurityPolicyConfigured()} + * and [EMAIL PROTECTED] #setSecurityPolicy(URL)}) assumes that the web container + * doesn't use a "double-equals" command-line assignment + * to override the security policy ( <i>e.g. </i>, + * <code>-Djava.security.policy==jspwiki.policy</code>). Note that Tomcat 4 + * and higher, when run using the "-security" option, does this. + * </p> + * <p> + * To interoperate with <i>any</i> container running with a security manager, the + * container's JVM security policy should include a short set of permission + * grant similar to the following: + * </p> + * <blockquote><code>keystore "jspwiki.jks";<br/> + * ...<br/> + * grant signedBy "jspwiki" {<br/> + * permission java.security.SecurityPermission, "getPolicy";<br/> + * permission java.security.SecurityPermission, "setPolicy";<br/> + * permission java.util.PropertyPermission "java.security.auth.login.config", "write";<br/> + * permission java.util.PropertyPermission "java.security.policy", "read,write";<br/> + * permission javax.security.auth.AuthPermission, "getLoginConfiguration";<br/> + * permission javax.security.auth.AuthPermission, "setLoginConfiguration";<br/> + * };</code> + * </blockquote> + * <p> + * The <code>signedBy</code> value should match the alias of a digital + * certificate in the named keystore ( <i>e.g. </i>, <code>jspwiki.jks</code>). + * If the full path to the keystore is not suppled, it is assumed to be in the + * same directory as the policy file. + * </p> + * + * @author Andrew Jaquith + * @since 2.3 + */ +public final class PolicyLoader +{ + protected static final Logger log = Logger.getLogger( PolicyLoader.class ); + + /** + * Private constructor to prevent direct instantiation. + */ + private PolicyLoader() + { + } + + /** + * <p> + * Returns <code>true</code> if the JAAS login configuration exists. + * Normally, JAAS is configured by setting the system property + * <code>java.security.auth.login.config</code> at JVM startup. + * </p> + * <p> + * This method attempts to perform a highly privileged operation. If the JVM + * runs with a SecurityManager, the following permission must be granted to + * the codesource containing this class: + * </p> + * <code><ul> + * <li>permission javax.security.auth.AuthPermission, + * "getLoginConfiguration"</li> + * </ul></code> + * + * @return <code>true</code> if + * [EMAIL PROTECTED] javax.security.auth.login.Configuration#getConfiguration()} + * is not <code>null</code> ; <code>false</code> otherwise. + * @throws SecurityException if the codesource containing this class posesses + * insufficient permmissions when running with a SecurityManager + */ + public static final boolean isJaasConfigured() throws SecurityException + { + Boolean configured = (Boolean) AccessController + .doPrivileged(new PrivilegedAction() { + + public Object run() + { + boolean isConfigured = false; + try + { + Configuration config = Configuration.getConfiguration(); + isConfigured = config != null; + } + catch (SecurityException e) {} + return Boolean.valueOf(isConfigured); + } + }); + return configured.booleanValue(); + } + + /** + * <p> + * Returns <code>true</code> if a custom Java security policy configuration + * exists. Normally, the Java security policy is configured by setting the + * system property <code>java.security.policy</code> at JVM startup. + * </p> + * <p> + * This method attempts to perform a highly privileged operation. If the JVM + * runs with a SecurityManager, the following permission must be granted to + * the codesource containing this class: + * </p> + * <code><ul> + * <li>permission java.util.PropertyPermission + * "java.security.policy", "read"</li> + * </ul></code> + * + * @return <code>true</code> if the system property + * <code>java.security.policy</code> is not <code>null</code>; + * <code>false</code> otherwise. + * @throws SecurityException if the codesource containing this class posesses + * insufficient permmissions when running with a SecurityManager + */ + public static final boolean isSecurityPolicyConfigured() throws SecurityException + { + String policy = (String) AccessController + .doPrivileged(new PrivilegedAction() { + + public Object run() + { + return System.getProperty("java.security.policy"); + } + }); + + if ( policy != null ) + { + log.info( "Java security policy already set to: " + policy + ". (Leaving it alone...)" ); + + // + // Do a bit of a sanity checks to help people who are not familiar with JAAS. + // + if( policy.startsWith("file:") ) policy = policy.substring("file:".length()); + + File f = new File(policy); + + if( !f.exists() ) + { + log.warn("You have set your 'java.security.policy' to point at '"+f.getAbsolutePath()+"', "+ + "but that file does not seem to exist. I'll continue anyway, since this may be "+ + "something specific to your servlet container. Just consider yourself warned." ); + } + + File jks = new File( f.getParentFile(), "jspwiki.jks" ); + + if( !jks.exists() || !jks.canRead() ) + { + log.warn("I could not locate the JSPWiki keystore ('jspwiki.jks') in the same directory "+ + "as your jspwiki.policy file. On many servlet containers, such as Tomcat, this "+ + "needs to be done. If you keep having access right permissions, please try "+ + "copying your WEB-INF/jspwiki.jks to "+f.getParentFile().getAbsolutePath() ); + } + else + { + log.info("Found 'jspwiki.jks' from '"+f.getParentFile().getAbsolutePath()+"'. If you are having "+ + "permission issues after an upgrade, please make sure that this file matches the one that "+ + "came with your distribution archive."); + } + + } + return policy != null; + } + + /** + * Sets the JAAS login configuration file, overwriting the existing + * configuration. If the configuration file pointed to by the URL does not + * exist, a SecurityException is thrown. + * <p> + * This method attempts to perform several highly privileged operations. If + * the JVM runs with a SecurityManager, the following permissions must be + * granted to the codesource containing this class: + * </p> + * <code><ul> + * <li>permission java.util.PropertyPermission + * "java.security.auth.login.config", "write"</li> + * <li>permission javax.security.auth.AuthPermission, + * "getLoginConfiguration"</li> + * <li>permission javax.security.auth.AuthPermission, + * "setLoginConfiguration"</li> + * </ul></code> + * + * @param url the URL of the login configuration file. If the URL contains + * properties such as <code>${java.home}</code>, they will be + * expanded. + * @throws SecurityException if: + * <ul> + * <li>the supplied URL is <code>null</code></li> + * <li>properties cannot be expanded</li> + * <li>the codesource containing this class does not posesses + * sufficient permmissions when running with a SecurityManager</li> + * </ul> + */ + public static final void setJaasConfiguration(final URL url) + throws SecurityException + { + if (url == null) + { + throw new SecurityException("URL for JAAS configuration cannot be null."); + } + + // Get JAAS configuration class; default is Sun provider + String defaultConfigClass; + defaultConfigClass = (String)AccessController.doPrivileged( + new PrivilegedAction() + { + public Object run() + { + return Security.getProperty("login.configuration.provider"); + } + }); + + if (defaultConfigClass == null) + { + defaultConfigClass = "com.sun.security.auth.login.ConfigFile"; + } + + // Now, set the new config + final String config_class = defaultConfigClass; + AccessController.doPrivileged(new PrivilegedAction() { + + public Object run() + { + // Remove old policy, then set our config URL and instantiate new instance + try + { + Configuration.setConfiguration(null); + System.setProperty("java.security.auth.login.config", url.toExternalForm()); + Configuration config = (Configuration)Class.forName(config_class).newInstance(); + Configuration.setConfiguration(config); + return null; + } + catch (Exception e) + { + throw new SecurityException(e.getMessage()); + } + } + }); + } + + /** + * <p> + * Sets the Java security policy, overwriting any custom policy settings. This + * method sets the value of the system property + * <code>java.security.policy</code> to the supplied URL, then calls + * [EMAIL PROTECTED] java.security.Policy#setPolicy(java.security.Policy)} with a + * newly-instantiated instance of + * <code>sun.security.provider.PolicyFile</code> (the J2SE default + * implementation). The new Policy, once set, reloads the default system + * policies enumerated by the <code>policy.url.<i>n</i></code> entries in + * <code><i>JAVA_HOME</i>/lib/security/java.policy</code>, followed by the + * user-supplied policy file. + * </p> + * <p> + * This method attempts to perform several highly privileged operations. If + * the JVM runs with a SecurityManager, the following permissions must be + * granted to the codesource containing this class: + * </p> + * <code><ul> + * <li>permission java.security.SecurityPermission, "getPolicy" + * </li> + * <li>permission java.security.SecurityPermission, "setPolicy" + * </li> + * <li>permission java.util.PropertyPermission} + * "java.security.policy", "write"</li> + * </ul></code> + * + * @param url the URL of the security policy file. If the URL contains + * properties such as <code>${java.home}</code>, they will be + * expanded. + * @throws SecurityException if: + * <ul> + * <li>the supplied URL is <code>null</code></li> + * <li>properties cannot be expanded</li> + * <li>the codesource containing this class does not posesses + * sufficient permmissions when running with a SecurityManager</li> + * <li>the JVM's current Policy implementation is not of type + * <code>sun.security.provider.PolicyFile</code></li> + * </ul> + */ + public static final void setSecurityPolicy(final URL url) throws SecurityException + { + if (url == null) + { + throw new SecurityException("URL for security policy cannot be null."); + } + + // Get policy class; default is Sun provider + String defaultPolicyClass; + defaultPolicyClass = (String)AccessController.doPrivileged( + new PrivilegedAction() { + public Object run() + { + return Security.getProperty("policy.provider"); + } + }); + + if (defaultPolicyClass == null) + { + defaultPolicyClass = "sun.security.provider.PolicyFile"; + } + + // Now, set the new policy + final String policyClass = defaultPolicyClass; + AccessController.doPrivileged(new PrivilegedAction() { + + public Object run() + { + // Remove old policy, then set our policy URL and instantiate new instance + try + { + Policy.setPolicy(null); + System.setProperty("java.security.policy", url.toExternalForm()); + + Policy policy = (Policy)Class.forName(policyClass).newInstance(); + Policy.setPolicy(policy); + return null; + } + catch (Exception e) + { + throw new SecurityException(e.getMessage()); + } + } + }); + } + +} Propchange: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PolicyLoader.java ------------------------------------------------------------------------------ svn:executable = * Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PrincipalComparator.java URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PrincipalComparator.java?rev=627255&view=auto ============================================================================== --- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PrincipalComparator.java (added) +++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/PrincipalComparator.java Tue Feb 12 21:53:55 2008 @@ -0,0 +1,50 @@ +/* + JSPWiki - a JSP-based WikiWiki clone. + + Copyright (C) 2001-2007 Janne Jalkanen ([EMAIL PROTECTED]) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package com.ecyrd.jspwiki.auth; + +import java.io.Serializable; +import java.security.Principal; +import java.text.Collator; +import java.util.Comparator; + +/** + * Comparator class for sorting objects of type Principal. + * Used for sorting arrays or collections of Principals. + * @since 2.3 + */ +public class PrincipalComparator + implements Comparator<Principal>, Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * Compares two Principal objects. + * @param o1 the first Principal + * @param o2 the second Principal + * @return the result of the comparison + * @see java.util.Comparator#compare(Object, Object) + */ + public int compare( Principal o1, Principal o2 ) + { + Collator collator = Collator.getInstance(); + return collator.compare( o1.getName(), o2.getName() ); + } + +}
