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