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