| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.wiki.auth.authorize; |
| |
| import org.apache.commons.lang3.ArrayUtils; |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.api.core.Engine; |
| import org.apache.wiki.api.core.Session; |
| import org.apache.wiki.api.exceptions.NoRequiredPropertyException; |
| import org.apache.wiki.api.exceptions.WikiException; |
| import org.apache.wiki.auth.AuthenticationManager; |
| import org.apache.wiki.auth.Authorizer; |
| import org.apache.wiki.auth.GroupPrincipal; |
| import org.apache.wiki.auth.NoSuchPrincipalException; |
| import org.apache.wiki.auth.UserManager; |
| import org.apache.wiki.auth.WikiPrincipal; |
| import org.apache.wiki.auth.WikiSecurityException; |
| import org.apache.wiki.auth.user.UserProfile; |
| import org.apache.wiki.event.WikiEvent; |
| import org.apache.wiki.event.WikiEventListener; |
| import org.apache.wiki.event.WikiEventManager; |
| import org.apache.wiki.event.WikiSecurityEvent; |
| import org.apache.wiki.ui.InputValidator; |
| import org.apache.wiki.util.ClassUtil; |
| |
| import java.security.Principal; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| |
| /** |
| * <p> |
| * Facade class for storing, retrieving and managing wiki groups on behalf of AuthorizationManager, JSPs and other presentation-layer |
| * classes. GroupManager works in collaboration with a back-end {@link GroupDatabase}, which persists groups to permanent storage. |
| * </p> |
| * <p> |
| * <em>Note: prior to JSPWiki 2.4.19, GroupManager was an interface; it is now a concrete, final class. The aspects of GroupManager |
| * which previously extracted group information from storage (e.g., wiki pages) have been refactored into the GroupDatabase interface.</em> |
| * </p> |
| * @since 2.4.19 |
| */ |
| public class DefaultGroupManager implements GroupManager, Authorizer, WikiEventListener { |
| |
| private static final Logger log = Logger.getLogger( DefaultGroupManager.class ); |
| |
| protected Engine m_engine; |
| |
| protected WikiEventListener m_groupListener; |
| |
| private GroupDatabase m_groupDatabase = null; |
| |
| /** Map with GroupPrincipals as keys, and Groups as values */ |
| private final Map< Principal, Group > m_groups = new HashMap<>(); |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Principal findRole( final String name ) { |
| try { |
| final Group group = getGroup( name ); |
| return group.getPrincipal(); |
| } catch( final NoSuchPrincipalException e ) { |
| return null; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Group getGroup( final String name ) throws NoSuchPrincipalException { |
| final Group group = m_groups.get( new GroupPrincipal( name ) ); |
| if( group != null ) { |
| return group; |
| } |
| throw new NoSuchPrincipalException( "Group " + name + " not found." ); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public GroupDatabase getGroupDatabase() throws WikiSecurityException { |
| if( m_groupDatabase != null ) { |
| return m_groupDatabase; |
| } |
| |
| String dbClassName = "<unknown>"; |
| String dbInstantiationError = null; |
| Throwable cause = null; |
| try { |
| final Properties props = m_engine.getWikiProperties(); |
| dbClassName = props.getProperty( PROP_GROUPDATABASE ); |
| if( dbClassName == null ) { |
| dbClassName = XMLGroupDatabase.class.getName(); |
| } |
| log.info( "Attempting to load group database class " + dbClassName ); |
| final Class< ? > dbClass = ClassUtil.findClass( "org.apache.wiki.auth.authorize", dbClassName ); |
| m_groupDatabase = ( GroupDatabase )dbClass.newInstance(); |
| m_groupDatabase.initialize( m_engine, m_engine.getWikiProperties() ); |
| log.info( "Group database initialized." ); |
| } catch( final ClassNotFoundException e ) { |
| log.error( "GroupDatabase class " + dbClassName + " cannot be found.", e ); |
| dbInstantiationError = "Failed to locate GroupDatabase class " + dbClassName; |
| cause = e; |
| } catch( final InstantiationException e ) { |
| log.error( "GroupDatabase class " + dbClassName + " cannot be created.", e ); |
| dbInstantiationError = "Failed to create GroupDatabase class " + dbClassName; |
| cause = e; |
| } catch( final IllegalAccessException e ) { |
| log.error( "You are not allowed to access group database class " + dbClassName + ".", e ); |
| dbInstantiationError = "Access GroupDatabase class " + dbClassName + " denied"; |
| cause = e; |
| } catch( final NoRequiredPropertyException e ) { |
| log.error( "Missing property: " + e.getMessage() + "." ); |
| dbInstantiationError = "Missing property: " + e.getMessage(); |
| cause = e; |
| } |
| |
| if( dbInstantiationError != null ) { |
| throw new WikiSecurityException( dbInstantiationError + " Cause: " + cause.getMessage(), cause ); |
| } |
| |
| return m_groupDatabase; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Principal[] getRoles() { |
| return m_groups.keySet().toArray( new Principal[ m_groups.size() ] ); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void initialize( final Engine engine, final Properties props ) throws WikiSecurityException { |
| m_engine = engine; |
| |
| try { |
| m_groupDatabase = getGroupDatabase(); |
| } catch( final WikiException e ) { |
| throw new WikiSecurityException( e.getMessage(), e ); |
| } |
| |
| // Load all groups from the database into the cache |
| final Group[] groups = m_groupDatabase.groups(); |
| synchronized( m_groups ) { |
| for( final Group group : groups ) { |
| // Add new group to cache; fire GROUP_ADD event |
| m_groups.put( group.getPrincipal(), group ); |
| fireEvent( WikiSecurityEvent.GROUP_ADD, group ); |
| } |
| } |
| |
| // Make the GroupManager listen for WikiEvents (WikiSecurityEvents for changed user profiles) |
| engine.getManager( UserManager.class ).addWikiEventListener( this ); |
| |
| // Success! |
| log.info( "Authorizer GroupManager initialized successfully; loaded " + groups.length + " group(s)." ); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean isUserInRole( final Session session, final Principal role ) { |
| // Always return false if session/role is null, or if role isn't a GroupPrincipal |
| if ( session == null || !( role instanceof GroupPrincipal ) || !session.isAuthenticated() ) { |
| return false; |
| } |
| |
| // Get the group we're examining |
| final Group group = m_groups.get( role ); |
| if( group == null ) { |
| return false; |
| } |
| |
| // Check each user principal to see if it belongs to the group |
| for( final Principal principal : session.getPrincipals() ) { |
| if( AuthenticationManager.isUserPrincipal( principal ) && group.isMember( principal ) ) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Group parseGroup( String name, String memberLine, final boolean create ) throws WikiSecurityException { |
| // If null name parameter, it's because someone's creating a new group |
| if( name == null ) { |
| if( create ) { |
| name = "MyGroup"; |
| } else { |
| throw new WikiSecurityException( "Group name cannot be blank." ); |
| } |
| } else if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) { |
| // Certain names are forbidden |
| throw new WikiSecurityException( "Illegal group name: " + name ); |
| } |
| name = name.trim(); |
| |
| // Normalize the member line |
| if( InputValidator.isBlank( memberLine ) ) { |
| memberLine = ""; |
| } |
| memberLine = memberLine.trim(); |
| |
| // Create or retrieve the group (may have been previously cached) |
| final Group group = new Group( name, m_engine.getApplicationName() ); |
| try { |
| final Group existingGroup = getGroup( name ); |
| |
| // If existing, clone it |
| group.setCreator( existingGroup.getCreator() ); |
| group.setCreated( existingGroup.getCreated() ); |
| group.setModifier( existingGroup.getModifier() ); |
| group.setLastModified( existingGroup.getLastModified() ); |
| for( final Principal existingMember : existingGroup.members() ) { |
| group.add( existingMember ); |
| } |
| } catch( final NoSuchPrincipalException e ) { |
| // It's a new group.... throw error if we don't create new ones |
| if( !create ) { |
| throw new NoSuchPrincipalException( "Group '" + name + "' does not exist." ); |
| } |
| } |
| |
| // If passed members not empty, overwrite |
| final String[] members = extractMembers( memberLine ); |
| if( members.length > 0 ) { |
| group.clear(); |
| for( final String member : members ) { |
| group.add( new WikiPrincipal( member ) ); |
| } |
| } |
| |
| return group; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void removeGroup( final String index ) throws WikiSecurityException { |
| if( index == null ) { |
| throw new IllegalArgumentException( "Group cannot be null." ); |
| } |
| |
| final Group group = m_groups.get( new GroupPrincipal( index ) ); |
| if( group == null ) { |
| throw new NoSuchPrincipalException( "Group " + index + " not found" ); |
| } |
| |
| // Delete the group |
| // TODO: need rollback procedure |
| synchronized( m_groups ) { |
| m_groups.remove( group.getPrincipal() ); |
| } |
| m_groupDatabase.delete( group ); |
| fireEvent( WikiSecurityEvent.GROUP_REMOVE, group ); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setGroup( final Session session, final Group group ) throws WikiSecurityException { |
| // TODO: check for appropriate permissions |
| |
| // If group already exists, delete it; fire GROUP_REMOVE event |
| final Group oldGroup = m_groups.get( group.getPrincipal() ); |
| if( oldGroup != null ) { |
| fireEvent( WikiSecurityEvent.GROUP_REMOVE, oldGroup ); |
| synchronized( m_groups ) { |
| m_groups.remove( oldGroup.getPrincipal() ); |
| } |
| } |
| |
| // Copy existing modifier info & timestamps |
| if( oldGroup != null ) { |
| group.setCreator( oldGroup.getCreator() ); |
| group.setCreated( oldGroup.getCreated() ); |
| group.setModifier( oldGroup.getModifier() ); |
| group.setLastModified( oldGroup.getLastModified() ); |
| } |
| |
| // Add new group to cache; announce GROUP_ADD event |
| synchronized( m_groups ) { |
| m_groups.put( group.getPrincipal(), group ); |
| } |
| fireEvent( WikiSecurityEvent.GROUP_ADD, group ); |
| |
| // Save the group to back-end database; if it fails, roll back to previous state. Note that the back-end |
| // MUST timestammp the create/modify fields in the Group. |
| try { |
| m_groupDatabase.save( group, session.getUserPrincipal() ); |
| } |
| |
| // We got an exception! Roll back... |
| catch( final WikiSecurityException e ) { |
| if( oldGroup != null ) { |
| // Restore previous version, re-throw... |
| fireEvent( WikiSecurityEvent.GROUP_REMOVE, group ); |
| fireEvent( WikiSecurityEvent.GROUP_ADD, oldGroup ); |
| synchronized( m_groups ) { |
| m_groups.put( oldGroup.getPrincipal(), oldGroup ); |
| } |
| throw new WikiSecurityException( e.getMessage() + " (rolled back to previous version).", e ); |
| } |
| // Re-throw security exception |
| throw new WikiSecurityException( e.getMessage(), e ); |
| } |
| } |
| |
| /** |
| * Extracts carriage-return separated members into a Set of String objects. |
| * |
| * @param memberLine the list of members |
| * @return the list of members |
| */ |
| protected String[] extractMembers( final String memberLine ) { |
| final Set< String > members = new HashSet<>(); |
| if( memberLine != null ) { |
| final StringTokenizer tok = new StringTokenizer( memberLine, "\n" ); |
| while( tok.hasMoreTokens() ) { |
| final String uid = tok.nextToken().trim(); |
| if( uid.length() > 0 ) { |
| members.add( uid ); |
| } |
| } |
| } |
| return members.toArray( new String[ members.size() ] ); |
| } |
| |
| // events processing ....................................................... |
| |
| /** {@inheritDoc} */ |
| @Override |
| public synchronized void addWikiEventListener( final WikiEventListener listener ) { |
| WikiEventManager.addWikiEventListener( this, listener ); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public synchronized void removeWikiEventListener( final WikiEventListener listener ) { |
| WikiEventManager.removeWikiEventListener( this, listener ); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void actionPerformed( final WikiEvent event ) { |
| if( !( event instanceof WikiSecurityEvent ) ) { |
| return; |
| } |
| |
| final WikiSecurityEvent se = ( WikiSecurityEvent )event; |
| if( se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED ) { |
| final Session session = se.getSrc(); |
| final UserProfile[] profiles = ( UserProfile[] )se.getTarget(); |
| final Principal[] oldPrincipals = new Principal[] { new WikiPrincipal( profiles[ 0 ].getLoginName() ), |
| new WikiPrincipal( profiles[ 0 ].getFullname() ), new WikiPrincipal( profiles[ 0 ].getWikiName() ) }; |
| final Principal newPrincipal = new WikiPrincipal( profiles[ 1 ].getFullname() ); |
| |
| // Examine each group |
| int groupsChanged = 0; |
| try { |
| for( final Group group : m_groupDatabase.groups() ) { |
| boolean groupChanged = false; |
| for( final Principal oldPrincipal : oldPrincipals ) { |
| if( group.isMember( oldPrincipal ) ) { |
| group.remove( oldPrincipal ); |
| group.add( newPrincipal ); |
| groupChanged = true; |
| } |
| } |
| if( groupChanged ) { |
| setGroup( session, group ); |
| groupsChanged++; |
| } |
| } |
| } catch( final WikiException e ) { |
| // Oooo! This is really bad... |
| log.error( "Could not change user name in Group lists because of GroupDatabase error:" + e.getMessage() ); |
| } |
| log.info( "Profile name change for '" + newPrincipal.toString() + "' caused " + groupsChanged + " groups to change also." ); |
| } |
| } |
| |
| } |