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