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}