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            if( oldGroup != null ) {
300                // Restore previous version, re-throw...
301                fireEvent( WikiSecurityEvent.GROUP_REMOVE, group );
302                fireEvent( WikiSecurityEvent.GROUP_ADD, oldGroup );
303                synchronized( m_groups ) {
304                    m_groups.put( oldGroup.getPrincipal(), oldGroup );
305                }
306                throw new WikiSecurityException( e.getMessage() + " (rolled back to previous version).", e );
307            }
308            // Re-throw security exception
309            throw new WikiSecurityException( e.getMessage(), e );
310        }
311    }
312
313    /** {@inheritDoc} */
314    @Override
315    public void validateGroup( final Context context, final Group group ) {
316        final InputValidator validator = new InputValidator( MESSAGES_KEY, context );
317
318        // Name cannot be null or one of the restricted names
319        try {
320            checkGroupName( context, group.getName() );
321        } catch( final WikiSecurityException e ) {
322        }
323
324        // Member names must be "safe" strings
325        final Principal[] members = group.members();
326        for( final Principal member : members ) {
327            validator.validateNotNull( member.getName(), "Full name", InputValidator.ID );
328        }
329    }
330
331    /** {@inheritDoc} */
332    @Override
333    public void checkGroupName( final Context context, final String name ) throws WikiSecurityException {
334        // TODO: groups cannot have the same name as a user
335
336        // Name cannot be null
337        final InputValidator validator = new InputValidator( MESSAGES_KEY, context );
338        validator.validateNotNull( name, "Group name" );
339
340        // Name cannot be one of the restricted names either
341        if( ArrayUtils.contains( Group.RESTRICTED_GROUPNAMES, name ) ) {
342            throw new WikiSecurityException( "The group name '" + name + "' is illegal. Choose another." );
343        }
344    }
345
346    /**
347     * Extracts carriage-return separated members into a Set of String objects.
348     *
349     * @param memberLine the list of members
350     * @return the list of members
351     */
352    protected String[] extractMembers( final String memberLine ) {
353        final Set< String > members = new HashSet<>();
354        if( memberLine != null ) {
355            final StringTokenizer tok = new StringTokenizer( memberLine, "\n" );
356            while( tok.hasMoreTokens() ) {
357                final String uid = tok.nextToken().trim();
358                if( !uid.isEmpty() ) {
359                    members.add( uid );
360                }
361            }
362        }
363        return members.toArray( new String[0] );
364    }
365
366    // events processing .......................................................
367
368    /** {@inheritDoc} */
369    @Override
370    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
371        WikiEventManager.addWikiEventListener( this, listener );
372    }
373
374    /** {@inheritDoc} */
375    @Override
376    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
377        WikiEventManager.removeWikiEventListener( this, listener );
378    }
379
380    /** {@inheritDoc} */
381    @Override
382    public void actionPerformed( final WikiEvent event ) {
383        if( !( event instanceof WikiSecurityEvent ) ) {
384            return;
385        }
386
387        final WikiSecurityEvent se = ( WikiSecurityEvent )event;
388        if( se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED ) {
389            final Session session = se.getSrc();
390            final UserProfile[] profiles = ( UserProfile[] )se.getTarget();
391            final Principal[] oldPrincipals = new Principal[] { new WikiPrincipal( profiles[ 0 ].getLoginName() ),
392                    new WikiPrincipal( profiles[ 0 ].getFullname() ), new WikiPrincipal( profiles[ 0 ].getWikiName() ) };
393            final Principal newPrincipal = new WikiPrincipal( profiles[ 1 ].getFullname() );
394
395            // Examine each group
396            int groupsChanged = 0;
397            try {
398                for( final Group group : m_groupDatabase.groups() ) {
399                    boolean groupChanged = false;
400                    for( final Principal oldPrincipal : oldPrincipals ) {
401                        if( group.isMember( oldPrincipal ) ) {
402                            group.remove( oldPrincipal );
403                            group.add( newPrincipal );
404                            groupChanged = true;
405                        }
406                    }
407                    if( groupChanged ) {
408                        setGroup( session, group );
409                        groupsChanged++;
410                    }
411                }
412            } catch( final WikiException e ) {
413                // Oooo! This is really bad...
414                LOG.error( "Could not change user name in Group lists because of GroupDatabase error:" + e.getMessage() );
415            }
416            LOG.info( "Profile name change for '" + newPrincipal + "' caused " + groupsChanged + " groups to change also." );
417        }
418    }
419
420}