    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            return null;
    086        }
    087    }
    088
    089    /** {@inheritDoc} */
    090    @Override
    091    public Group getGroup( final String name ) throws NoSuchPrincipalException {
    092        final Group group = m_groups.get( new GroupPrincipal( name ) );
    093        if( group != null ) {
    094            return group;
    095        }
    096        throw new NoSuchPrincipalException( "Group " + name + " not found." );
    097    }
    098
    099    /** {@inheritDoc} */
    100    @Override
    101    public GroupDatabase getGroupDatabase() throws WikiSecurityException {
    102        if( m_groupDatabase != null ) {
    103            return m_groupDatabase;
    104        }
    105
    106        String dbClassName = "<unknown>";
    107        String dbInstantiationError = null;
    108        Throwable cause = null;
    109        try {
    110            final Properties props = m_engine.getWikiProperties();
    111            dbClassName = props.getProperty( PROP_GROUPDATABASE );
    112            if( dbClassName == null ) {
    113                dbClassName = XMLGroupDatabase.class.getName();
    114            }
    115            LOG.info( "Attempting to load group database class {}", dbClassName );
    116            m_groupDatabase = ClassUtil.buildInstance( "org.apache.wiki.auth.authorize", dbClassName );
    117            m_groupDatabase.initialize( m_engine, m_engine.getWikiProperties() );
    118            LOG.info( "Group database initialized." );
    119        } catch( final ReflectiveOperationException e ) {
    120            LOG.error( "UserDatabase {} cannot be instantiated", dbClassName, e );
    121            dbInstantiationError = "Access GroupDatabase class " + dbClassName + " denied";
    122            cause = e;
    123        } catch( final NoRequiredPropertyException e ) {
    124            LOG.error( "Missing property: " + e.getMessage() + "." );
    125            dbInstantiationError = "Missing property: " + e.getMessage();
    126            cause = e;
    127        }
    128
    129        if( dbInstantiationError != null ) {
    130            throw new WikiSecurityException( dbInstantiationError + " Cause: " + cause.getMessage(), cause );
    131        }
    132
    133        return m_groupDatabase;
    134    }
    135
    136    /** {@inheritDoc} */
    137    @Override
    138    public Principal[] getRoles() {
    139        return m_groups.keySet().toArray( new Principal[0] );
    140    }
    141
    142    /** {@inheritDoc} */
    143    @Override
    144    public void initialize( final Engine engine, final Properties props ) throws WikiSecurityException {
    145        m_engine = engine;
    146
    147        try {
    148            m_groupDatabase = getGroupDatabase();
    149        } catch( final WikiException e ) {
    150            throw new WikiSecurityException( e.getMessage(), e );
    151        }
    152
    153        // Load all groups from the database into the cache
    154        final Group[] groups = m_groupDatabase.groups();
    155        synchronized( m_groups ) {
    156            for( final Group group : groups ) {
    157                // Add new group to cache; fire GROUP_ADD event
    158                m_groups.put( group.getPrincipal(), group );
    159                fireEvent( WikiSecurityEvent.GROUP_ADD, group );
    160            }
    161        }
    162
    163        // Make the GroupManager listen for WikiEvents (WikiSecurityEvents for changed user profiles)
    164        engine.getManager( UserManager.class ).addWikiEventListener( this );
    165
    166        // Success!
    167        LOG.info( "Authorizer GroupManager initialized successfully; loaded " + groups.length + " group(s)." );
    168    }
    169
    170    /** {@inheritDoc} */
    171    @Override
    172    public boolean isUserInRole( final Session session, final Principal role ) {
    173        // Always return false if session/role is null, or if role isn't a GroupPrincipal
    174        if ( session == null || !( role instanceof GroupPrincipal ) || !session.isAuthenticated() ) {
    175            return false;
    176        }
    177
    178        // Get the group we're examining
    179        final Group group = m_groups.get( role );
    180        if( group == null ) {
    181            return false;
    182        }
    183
    184        // Check each user principal to see if it belongs to the group
    185        return Arrays.stream(session.getPrincipals()).anyMatch(principal -> AuthenticationManager.isUserPrincipal(principal) && group.isMember(principal));
    186    }
    187
    188    /** {@inheritDoc} */
    189    @Override
    190    public Group parseGroup( String name, String memberLine, final boolean create ) throws WikiSecurityException {
    191        // If null name parameter, it's because someone's creating a new group
    192        if( name == null ) {
    193            if( create ) {
    194                name = "MyGroup";
    195            } else {
    196                throw new WikiSecurityException( "Group name cannot be blank." );
    197            }
    198        } else if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) {
    199            // Certain names are forbidden
    200            throw new WikiSecurityException( "Illegal group name: " + name );
    201        }
    202        name = name.trim();
    203
    204        // Normalize the member line
    205        if( InputValidator.isBlank( memberLine ) ) {
    206            memberLine = "";
    207        }
    208        memberLine = memberLine.trim();
    209
    210        // Create or retrieve the group (may have been previously cached)
    211        final Group group = new Group( name, m_engine.getApplicationName() );
    212        try {
    213            final Group existingGroup = getGroup( name );
    214
    215            // If existing, clone it
    216            group.setCreator( existingGroup.getCreator() );
    217            group.setCreated( existingGroup.getCreated() );
    218            group.setModifier( existingGroup.getModifier() );
    219            group.setLastModified( existingGroup.getLastModified() );
    220            for( final Principal existingMember : existingGroup.members() ) {
    221                group.add( existingMember );
    222            }
    223        } catch( final NoSuchPrincipalException e ) {
    224            // It's a new group.... throw error if we don't create new ones
    225            if( !create ) {
    226                throw new NoSuchPrincipalException( "Group '" + name + "' does not exist." );
    227            }
    228        }
    229
    230        // If passed members not empty, overwrite
    231        final String[] members = extractMembers( memberLine );
    232        if( members.length > 0 ) {
    233            group.clear();
    234            for( final String member : members ) {
    235                group.add( new WikiPrincipal( member ) );
    236            }
    237        }
    238
    239        return group;
    240    }
    241
    242    /** {@inheritDoc} */
    243    @Override
    244    public void removeGroup( final String index ) throws WikiSecurityException {
    245        if( index == null ) {
    246            throw new IllegalArgumentException( "Group cannot be null." );
    247        }
    248
    249        final Group group = m_groups.get( new GroupPrincipal( index ) );
    250        if( group == null ) {
    251            throw new NoSuchPrincipalException( "Group " + index + " not found" );
    252        }
    253
    254        // Delete the group
    255        // TODO: need rollback procedure
    256        synchronized( m_groups ) {
    257            m_groups.remove( group.getPrincipal() );
    258        }
    259        m_groupDatabase.delete( group );
    260        fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
    261    }
    262
    263    /** {@inheritDoc} */
    264    @Override
    265    public void setGroup( final Session session, final Group group ) throws WikiSecurityException {
    266        // TODO: check for appropriate permissions
    267
    268        // If group already exists, delete it; fire GROUP_REMOVE event
    269        final Group oldGroup = m_groups.get( group.getPrincipal() );
    270        if( oldGroup != null ) {
    271            fireEvent( WikiSecurityEvent.GROUP_REMOVE, oldGroup );
    272            synchronized( m_groups ) {
    273                m_groups.remove( oldGroup.getPrincipal() );
    274            }
    275        }
    276
    277        // Copy existing modifier info & timestamps
    278        if( oldGroup != null ) {
    279            group.setCreator( oldGroup.getCreator() );
    280            group.setCreated( oldGroup.getCreated() );
    281            group.setModifier( oldGroup.getModifier() );
    282            group.setLastModified( oldGroup.getLastModified() );
    283        }
    284
    285        // Add new group to cache; announce GROUP_ADD event
    286        synchronized( m_groups ) {
    287            m_groups.put( group.getPrincipal(), group );
    288        }
    289        fireEvent( WikiSecurityEvent.GROUP_ADD, group );
    290
    291        // Save the group to back-end database; if it fails, roll back to previous state. Note that the back-end
    292        // MUST timestammp the create/modify fields in the Group.
    293        try {
    294            m_groupDatabase.save( group, session.getUserPrincipal() );
    295        }
    296
    297        // We got an exception! Roll back...
    298        catch( final WikiSecurityException e ) {
    299            LOG.warn("setGroup failed, rolling back changes", e);
    300            if( oldGroup != null ) {
    301                // Restore previous version, re-throw...
    302                fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
    303                fireEvent( WikiSecurityEvent.GROUP_ADD, oldGroup );
    304                synchronized( m_groups ) {
    305                    m_groups.put( oldGroup.getPrincipal(), oldGroup );
    306                }
    307                throw new WikiSecurityException( e.getMessage() + " (rolled back to previous version).", e );
    308            }
    309            // Re-throw security exception
    310            throw new WikiSecurityException( e.getMessage(), e );
    311        }
    312    }
    313
    314    /** {@inheritDoc} */
    315    @Override
    316    public void validateGroup( final Context context, final Group group ) {
    317        final InputValidator validator = new InputValidator( MESSAGES_KEY, context );
    318
    319        // Name cannot be null or one of the restricted names
    320        try {
    321            checkGroupName( context, group.getName() );
    322        } catch( final WikiSecurityException e ) {
    323        }
    324
    325        // Member names must be "safe" strings
    326        final Principal[] members = group.members();
    327        for( final Principal member : members ) {
    328            validator.validateNotNull( member.getName(), "Full name", InputValidator.ID );
    329        }
    330    }
    331
    332    /** {@inheritDoc} */
    333    @Override
    334    public void checkGroupName( final Context context, final String name ) throws WikiSecurityException {
    335        // TODO: groups cannot have the same name as a user
    336
    337        // Name cannot be null
    338        final InputValidator validator = new InputValidator( MESSAGES_KEY, context );
    339        validator.validateNotNull( name, "Group name" );
    340
    341        // Name cannot be one of the restricted names either
    342        if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) {
    343            throw new WikiSecurityException( "The group name '" + name + "' is illegal. Choose another." );
    344        }
    345    }
    346
    347    /**
    348     * Extracts carriage-return separated members into a Set of String objects.
    349     *
    350     * @param memberLine the list of members
    351     * @return the list of members
    352     */
    353    protected String[] extractMembers( final String memberLine ) {
    354        final Set< String > members = new HashSet<>();
    355        if( memberLine != null ) {
    356            final StringTokenizer tok = new StringTokenizer( memberLine, "\n" );
    357            while( tok.hasMoreTokens() ) {
    358                final String uid = tok.nextToken().trim();
    359                if( !uid.isEmpty() ) {
    360                    members.add( uid );
    361                }
    362            }
    363        }
    364        return members.toArray( new String[0] );
    365    }
    366
    367    // events processing .......................................................
    368
    369    /** {@inheritDoc} */
    370    @Override
    371    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
    372        WikiEventManager.addWikiEventListener( this, listener );
    373    }
    374
    375    /** {@inheritDoc} */
    376    @Override
    377    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
    378        WikiEventManager.removeWikiEventListener( this, listener );
    379    }
    380
    381    /** {@inheritDoc} */
    382    @Override
    383    public void actionPerformed( final WikiEvent event ) {
    384        if( !( event instanceof WikiSecurityEvent ) ) {
    385            return;
    386        }
    387
    388        final WikiSecurityEvent se = ( WikiSecurityEvent )event;
    389        if( se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED ) {
    390            final Session session = se.getSrc();
    391            final UserProfile[] profiles = ( UserProfile[] )se.getTarget();
    392            final Principal[] oldPrincipals = new Principal[] { new WikiPrincipal( profiles[ 0 ].getLoginName() ),
    393                    new WikiPrincipal( profiles[ 0 ].getFullname() ), new WikiPrincipal( profiles[ 0 ].getWikiName() ) };
    394            final Principal newPrincipal = new WikiPrincipal( profiles[ 1 ].getFullname() );
    395
    396            // Examine each group
    397            int groupsChanged = 0;
    398            try {
    399                for( final Group group : m_groupDatabase.groups() ) {
    400                    boolean groupChanged = false;
    401                    for( final Principal oldPrincipal : oldPrincipals ) {
    402                        if( group.isMember( oldPrincipal ) ) {
    403                            group.remove( oldPrincipal );
    404                            group.add( newPrincipal );
    405                            groupChanged = true;
    406                        }
    407                    }
    408                    if( groupChanged ) {
    409                        setGroup( session, group );
    410                        groupsChanged++;
    411                    }
    412                }
    413            } catch( final WikiException e ) {
    414                // Oooo! This is really bad...
    415                LOG.error( "Could not change user name in Group lists because of GroupDatabase error:" + e.getMessage() );
    416            }
    417            LOG.info( "Profile name change for '" + newPrincipal + "' caused " + groupsChanged + " groups to change also." );
    418        }
    419    }
    420
    421}