    001/*
    002    Licensed to the Apache Software Foundation (ASF) under one
    003    or more contributor license agreements.  See the NOTICE file
    004    distributed with this work for additional information
    005    regarding copyright ownership.  The ASF licenses this file
    006    to you under the Apache License, Version 2.0 (the
    007    "License"); you may not use this file except in compliance
    008    with the License.  You may obtain a copy of the License at
    009
    010       http://www.apache.org/licenses/LICENSE-2.0
    011
    012    Unless required by applicable law or agreed to in writing,
    013    software distributed under the License is distributed on an
    014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015    KIND, either express or implied.  See the License for the
    016    specific language governing permissions and limitations
    017    under the License.
    018 */
    019package org.apache.wiki.auth.authorize;
    020
    021import org.apache.commons.lang3.ArrayUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.wiki.api.core.Context;
    025import org.apache.wiki.api.core.Engine;
    026import org.apache.wiki.api.core.Session;
    027import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    028import org.apache.wiki.api.exceptions.WikiException;
    029import org.apache.wiki.auth.AuthenticationManager;
    030import org.apache.wiki.auth.Authorizer;
    031import org.apache.wiki.auth.GroupPrincipal;
    032import org.apache.wiki.auth.NoSuchPrincipalException;
    033import org.apache.wiki.auth.UserManager;
    034import org.apache.wiki.auth.WikiPrincipal;
    035import org.apache.wiki.auth.WikiSecurityException;
    036import org.apache.wiki.auth.user.UserProfile;
    037import org.apache.wiki.event.WikiEvent;
    038import org.apache.wiki.event.WikiEventListener;
    039import org.apache.wiki.event.WikiEventManager;
    040import org.apache.wiki.event.WikiSecurityEvent;
    041import org.apache.wiki.ui.InputValidator;
    042import org.apache.wiki.util.ClassUtil;
    043
    044import java.security.Principal;
    045import java.util.Arrays;
    046import java.util.HashMap;
    047import java.util.HashSet;
    048import java.util.Map;
    049import java.util.Properties;
    050import java.util.Set;
    051import java.util.StringTokenizer;
    052
    053
    054/**
    055 * <p>
    056 * Facade class for storing, retrieving and managing wiki groups on behalf of AuthorizationManager, JSPs and other presentation-layer
    057 * classes. GroupManager works in collaboration with a back-end {@link GroupDatabase}, which persists groups to permanent storage.
    058 * </p>
    059 * <p>
    060 * <em>Note: prior to JSPWiki 2.4.19, GroupManager was an interface; it is now a concrete, final class. The aspects of GroupManager
    061 * which previously extracted group information from storage (e.g., wiki pages) have been refactored into the GroupDatabase interface.</em>
    062 * </p>
    063 * @since 2.4.19
    064 */
    065public class DefaultGroupManager implements GroupManager, Authorizer, WikiEventListener {
    066
    067    private static final Logger LOG = LogManager.getLogger( DefaultGroupManager.class );
    068
    069    protected Engine m_engine;
    070
    071    protected WikiEventListener m_groupListener;
    072
    073    private GroupDatabase m_groupDatabase;
    074
    075    /** Map with GroupPrincipals as keys, and Groups as values */
    076    private final Map< Principal, Group > m_groups = new HashMap<>();
    077
    078    /** {@inheritDoc} */
    079    @Override
    080    public Principal findRole( final String name ) {
    081        try {
    082            final Group group = getGroup( name );
    083            return group.getPrincipal();
    084        } catch( final NoSuchPrincipalException e ) {
    085            LOG.debug(e.getMessage(), e);
    086            return null;
    087        }
    088    }
    089
    090    /** {@inheritDoc} */
    091    @Override
    092    public Group getGroup( final String name ) throws NoSuchPrincipalException {
    093        final Group group = m_groups.get( new GroupPrincipal( name ) );
    094        if( group != null ) {
    095            return group;
    096        }
    097        throw new NoSuchPrincipalException( "Group " + name + " not found." );
    098    }
    099
    100    /** {@inheritDoc} */
    101    @Override
    102    public GroupDatabase getGroupDatabase() throws WikiSecurityException {
    103        if( m_groupDatabase != null ) {
    104            return m_groupDatabase;
    105        }
    106
    107        String dbClassName = "<unknown>";
    108        String dbInstantiationError = null;
    109        Throwable cause = null;
    110        try {
    111            final Properties props = m_engine.getWikiProperties();
    112            dbClassName = props.getProperty( PROP_GROUPDATABASE );
    113            if( dbClassName == null ) {
    114                dbClassName = XMLGroupDatabase.class.getName();
    115            }
    116            LOG.info( "Attempting to load group database class {}", dbClassName );
    117            m_groupDatabase = ClassUtil.buildInstance( "org.apache.wiki.auth.authorize", dbClassName );
    118            m_groupDatabase.initialize( m_engine, m_engine.getWikiProperties() );
    119            LOG.info( "Group database initialized." );
    120        } catch( final ReflectiveOperationException e ) {
    121            LOG.error( "UserDatabase {} cannot be instantiated", dbClassName, e );
    122            dbInstantiationError = "Access GroupDatabase class " + dbClassName + " denied";
    123            cause = e;
    124        } catch( final NoRequiredPropertyException e ) {
    125            LOG.error( "Missing property: " + e.getMessage() + "." );
    126            dbInstantiationError = "Missing property: " + e.getMessage();
    127            cause = e;
    128        }
    129
    130        if( dbInstantiationError != null ) {
    131            throw new WikiSecurityException( dbInstantiationError + " Cause: " + cause.getMessage(), cause );
    132        }
    133
    134        return m_groupDatabase;
    135    }
    136
    137    /** {@inheritDoc} */
    138    @Override
    139    public Principal[] getRoles() {
    140        return m_groups.keySet().toArray( new Principal[0] );
    141    }
    142
    143    /** {@inheritDoc} */
    144    @Override
    145    public void initialize( final Engine engine, final Properties props ) throws WikiSecurityException {
    146        m_engine = engine;
    147
    148        try {
    149            m_groupDatabase = getGroupDatabase();
    150        } catch( final WikiException e ) {
    151            throw new WikiSecurityException( e.getMessage(), e );
    152        }
    153
    154        // Load all groups from the database into the cache
    155        final Group[] groups = m_groupDatabase.groups();
    156        synchronized( m_groups ) {
    157            for( final Group group : groups ) {
    158                // Add new group to cache; fire GROUP_ADD event
    159                m_groups.put( group.getPrincipal(), group );
    160                fireEvent( WikiSecurityEvent.GROUP_ADD, group );
    161            }
    162        }
    163
    164        // Make the GroupManager listen for WikiEvents (WikiSecurityEvents for changed user profiles)
    165        engine.getManager( UserManager.class ).addWikiEventListener( this );
    166
    167        // Success!
    168        LOG.info( "Authorizer GroupManager initialized successfully; loaded " + groups.length + " group(s)." );
    169    }
    170
    171    /** {@inheritDoc} */
    172    @Override
    173    public boolean isUserInRole( final Session session, final Principal role ) {
    174        // Always return false if session/role is null, or if role isn't a GroupPrincipal
    175        if ( session == null || !( role instanceof GroupPrincipal ) || !session.isAuthenticated() ) {
    176            return false;
    177        }
    178
    179        // Get the group we're examining
    180        final Group group = m_groups.get( role );
    181        if( group == null ) {
    182            return false;
    183        }
    184
    185        // Check each user principal to see if it belongs to the group
    186        return Arrays.stream(session.getPrincipals()).anyMatch(principal -> AuthenticationManager.isUserPrincipal(principal) && group.isMember(principal));
    187    }
    188
    189    /** {@inheritDoc} */
    190    @Override
    191    public Group parseGroup( String name, String memberLine, final boolean create ) throws WikiSecurityException {
    192        // If null name parameter, it's because someone's creating a new group
    193        if( name == null ) {
    194            if( create ) {
    195                name = "MyGroup";
    196            } else {
    197                //TODO i18n
    198                throw new WikiSecurityException( "Group name cannot be blank." );
    199            }
    200        } else if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) {
    201            // Certain names are forbidden
    202            throw new WikiSecurityException( "Illegal group name: " + name );
    203        }
    204        name = name.trim();
    205
    206        // Normalize the member line
    207        if( InputValidator.isBlank( memberLine ) ) {
    208            memberLine = "";
    209        }
    210        memberLine = memberLine.trim();
    211
    212        // Create or retrieve the group (may have been previously cached)
    213        final Group group = new Group( name, m_engine.getApplicationName() );
    214        try {
    215            final Group existingGroup = getGroup( name );
    216
    217            // If existing, clone it
    218            group.setCreator( existingGroup.getCreator() );
    219            group.setCreated( existingGroup.getCreated() );
    220            group.setModifier( existingGroup.getModifier() );
    221            group.setLastModified( existingGroup.getLastModified() );
    222            for( final Principal existingMember : existingGroup.members() ) {
    223                group.add( existingMember );
    224            }
    225        } catch( final NoSuchPrincipalException e ) {
    226            // It's a new group.... throw error if we don't create new ones
    227            if( !create ) {
    228                //TODO i18n
    229                throw new NoSuchPrincipalException( "Group '" + name + "' does not exist." );
    230            }
    231        }
    232
    233        // If passed members not empty, overwrite
    234        final String[] members = extractMembers( memberLine );
    235        if( members.length > 0 ) {
    236            group.clear();
    237            for( final String member : members ) {
    238                group.add( new WikiPrincipal( member ) );
    239            }
    240        }
    241
    242        if ("false".equalsIgnoreCase(m_engine.getWikiProperties().getProperty(Engine.PROP_USE_2_X_ACL_LOGIC, "false"))) {
    243            //check to ensure that the group name does not conflict with any existing user account login, email or wiki name
    244            UserManager userManger = m_engine.getManager(UserManager.class);
    245            try { userManger.getUserDatabase().findByEmail(name);
    246                throw new WikiSecurityException( "Group name conflicts with a user account" );
    247            }catch (NoSuchPrincipalException e) {
    248                //no issues here
    249            }
    250            try { userManger.getUserDatabase().findByLoginName(name);
    251                throw new WikiSecurityException( "Group name conflicts with a user account" );
    252            }catch (NoSuchPrincipalException e) {
    253                //no issues here
    254            }
    255            try { userManger.getUserDatabase().findByWikiName(name);
    256                throw new WikiSecurityException( "Group name conflicts with a user account" );
    257            }catch (NoSuchPrincipalException e) {
    258                //no issues here
    259            }
    260        }
    261
    262        return group;
    263    }
    264
    265    /** {@inheritDoc} */
    266    @Override
    267    public void removeGroup( final String index ) throws WikiSecurityException {
    268        if( index == null ) {
    269            throw new IllegalArgumentException( "Group cannot be null." );
    270        }
    271
    272        final Group group = m_groups.get( new GroupPrincipal( index ) );
    273        if( group == null ) {
    274            throw new NoSuchPrincipalException( "Group " + index + " not found" );
    275        }
    276
    277        // Delete the group
    278        // TODO: need rollback procedure
    279        synchronized( m_groups ) {
    280            m_groups.remove( group.getPrincipal() );
    281        }
    282        m_groupDatabase.delete( group );
    283        fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
    284    }
    285
    286    /** {@inheritDoc} */
    287    @Override
    288    public void setGroup( final Session session, final Group group ) throws WikiSecurityException {
    289        // TODO: check for appropriate permissions
    290
    291        // If group already exists, delete it; fire GROUP_REMOVE event
    292        final Group oldGroup = m_groups.get( group.getPrincipal() );
    293        if( oldGroup != null ) {
    294            fireEvent( WikiSecurityEvent.GROUP_REMOVE, oldGroup );
    295            synchronized( m_groups ) {
    296                m_groups.remove( oldGroup.getPrincipal() );
    297            }
    298        }
    299
    300        // Copy existing modifier info & timestamps
    301        if( oldGroup != null ) {
    302            group.setCreator( oldGroup.getCreator() );
    303            group.setCreated( oldGroup.getCreated() );
    304            group.setModifier( oldGroup.getModifier() );
    305            group.setLastModified( oldGroup.getLastModified() );
    306        }
    307
    308        // Add new group to cache; announce GROUP_ADD event
    309        synchronized( m_groups ) {
    310            m_groups.put( group.getPrincipal(), group );
    311        }
    312        fireEvent( WikiSecurityEvent.GROUP_ADD, group );
    313
    314        // Save the group to back-end database; if it fails, roll back to previous state. Note that the back-end
    315        // MUST timestammp the create/modify fields in the Group.
    316        try {
    317            m_groupDatabase.save( group, session.getUserPrincipal() );
    318        }
    319
    320        // We got an exception! Roll back...
    321        catch( final WikiSecurityException e ) {
    322            LOG.warn("setGroup failed, rolling back changes", e);
    323            if( oldGroup != null ) {
    324                // Restore previous version, re-throw...
    325                fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
    326                fireEvent( WikiSecurityEvent.GROUP_ADD, oldGroup );
    327                synchronized( m_groups ) {
    328                    m_groups.put( oldGroup.getPrincipal(), oldGroup );
    329                }
    330                throw new WikiSecurityException( e.getMessage() + " (rolled back to previous version).", e );
    331            }
    332            // Re-throw security exception
    333            throw new WikiSecurityException( e.getMessage(), e );
    334        }
    335    }
    336
    337    /** {@inheritDoc} */
    338    @Override
    339    public void validateGroup( final Context context, final Group group ) {
    340        final InputValidator validator = new InputValidator( MESSAGES_KEY, context );
    341
    342        // Name cannot be null or one of the restricted names
    343        try {
    344            checkGroupName( context, group.getName() );
    345        } catch( final WikiSecurityException e ) {
    346            LOG.debug(e.getMessage(), e);
    347        }
    348
    349        // Member names must be "safe" strings
    350        final Principal[] members = group.members();
    351        for( final Principal member : members ) {
    352            validator.validateNotNull( member.getName(), "Full name", InputValidator.ID );
    353        }
    354    }
    355
    356    /** {@inheritDoc} */
    357    @Override
    358    public void checkGroupName( final Context context, final String name ) throws WikiSecurityException {
    359        // TODO: groups cannot have the same name as a user
    360
    361        // Name cannot be null
    362        final InputValidator validator = new InputValidator( MESSAGES_KEY, context );
    363        validator.validateNotNull( name, "Group name" );
    364
    365        // Name cannot be one of the restricted names either
    366        if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) {
    367            throw new WikiSecurityException( "The group name '" + name + "' is illegal. Choose another." );
    368        }
    369    }
    370
    371    /**
    372     * Extracts carriage-return separated members into a Set of String objects.
    373     *
    374     * @param memberLine the list of members
    375     * @return the list of members
    376     */
    377    protected String[] extractMembers( final String memberLine ) {
    378        final Set< String > members = new HashSet<>();
    379        if( memberLine != null ) {
    380            final StringTokenizer tok = new StringTokenizer( memberLine, "\n" );
    381            while( tok.hasMoreTokens() ) {
    382                final String uid = tok.nextToken().trim();
    383                if( !uid.isEmpty() ) {
    384                    members.add( uid );
    385                }
    386            }
    387        }
    388        return members.toArray( new String[0] );
    389    }
    390
    391    // events processing .......................................................
    392
    393    /** {@inheritDoc} */
    394    @Override
    395    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
    396        WikiEventManager.addWikiEventListener( this, listener );
    397    }
    398
    399    /** {@inheritDoc} */
    400    @Override
    401    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
    402        WikiEventManager.removeWikiEventListener( this, listener );
    403    }
    404
    405    /** {@inheritDoc} */
    406    @Override
    407    public void actionPerformed( final WikiEvent event ) {
    408        if( !( event instanceof WikiSecurityEvent ) ) {
    409            return;
    410        }
    411
    412        final WikiSecurityEvent se = ( WikiSecurityEvent )event;
    413        if( se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED ) {
    414            final Session session = se.getSrc();
    415            final UserProfile[] profiles = ( UserProfile[] )se.getTarget();
    416            final Principal[] oldPrincipals = new Principal[] { new WikiPrincipal( profiles[ 0 ].getLoginName() ),
    417                    new WikiPrincipal( profiles[ 0 ].getFullname() ), new WikiPrincipal( profiles[ 0 ].getWikiName() ) };
    418            final Principal newPrincipal = new WikiPrincipal( profiles[ 1 ].getFullname() );
    419
    420            // Examine each group
    421            int groupsChanged = 0;
    422            try {
    423                for( final Group group : m_groupDatabase.groups() ) {
    424                    boolean groupChanged = false;
    425                    for( final Principal oldPrincipal : oldPrincipals ) {
    426                        if( group.isMember( oldPrincipal ) ) {
    427                            group.remove( oldPrincipal );
    428                            group.add( newPrincipal );
    429                            groupChanged = true;
    430                        }
    431                    }
    432                    if( groupChanged ) {
    433                        setGroup( session, group );
    434                        groupsChanged++;
    435                    }
    436                }
    437            } catch( final WikiException e ) {
    438                // Oooo! This is really bad...
    439                LOG.error( "Could not change user name in Group lists because of GroupDatabase error:" + e.getMessage() );
    440            }
    441            LOG.info( "Profile name change for '" + newPrincipal + "' caused " + groupsChanged + " groups to change also." );
    442        }
    443    }
    444
    445}