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;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.logging.log4j.LogManager;
023import org.apache.logging.log4j.Logger;
024import org.apache.wiki.ajax.AjaxUtil;
025import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
026import org.apache.wiki.ajax.WikiAjaxServlet;
027import org.apache.wiki.api.core.Context;
028import org.apache.wiki.api.core.Engine;
029import org.apache.wiki.api.core.Session;
030import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
031import org.apache.wiki.api.exceptions.WikiException;
032import org.apache.wiki.api.filters.PageFilter;
033import org.apache.wiki.auth.permissions.AllPermission;
034import org.apache.wiki.auth.permissions.WikiPermission;
035import org.apache.wiki.auth.user.DummyUserDatabase;
036import org.apache.wiki.auth.user.DuplicateUserException;
037import org.apache.wiki.auth.user.UserDatabase;
038import org.apache.wiki.auth.user.UserProfile;
039import org.apache.wiki.event.WikiEventListener;
040import org.apache.wiki.event.WikiEventManager;
041import org.apache.wiki.event.WikiSecurityEvent;
042import org.apache.wiki.filters.FilterManager;
043import org.apache.wiki.filters.SpamFilter;
044import org.apache.wiki.i18n.InternationalizationManager;
045import org.apache.wiki.pages.PageManager;
046import org.apache.wiki.preferences.Preferences;
047import org.apache.wiki.tasks.TasksManager;
048import org.apache.wiki.ui.InputValidator;
049import org.apache.wiki.util.ClassUtil;
050import org.apache.wiki.util.TextUtil;
051import org.apache.wiki.workflow.Decision;
052import org.apache.wiki.workflow.DecisionRequiredException;
053import org.apache.wiki.workflow.Fact;
054import org.apache.wiki.workflow.Step;
055import org.apache.wiki.workflow.Workflow;
056import org.apache.wiki.workflow.WorkflowBuilder;
057import org.apache.wiki.workflow.WorkflowManager;
058
059import javax.servlet.ServletException;
060import javax.servlet.http.HttpServletRequest;
061import javax.servlet.http.HttpServletResponse;
062import java.io.IOException;
063import java.security.Permission;
064import java.security.Principal;
065import java.text.MessageFormat;
066import java.util.List;
067import java.util.Map;
068import java.util.NoSuchElementException;
069import java.util.Properties;
070import java.util.ResourceBundle;
071import java.util.WeakHashMap;
072
073
074/**
075 * Default implementation for {@link UserManager}.
076 *
077 * @since 2.3
078 */
079public class DefaultUserManager implements UserManager {
080
081    private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user";
082    private static final String SESSION_MESSAGES = "profile";
083    private static final String PARAM_EMAIL = "email";
084    private static final String PARAM_FULLNAME = "fullname";
085    private static final String PARAM_PASSWORD = "password";
086    private static final String PARAM_LOGINNAME = "loginname";
087    private static final String UNKNOWN_CLASS = "<unknown>";
088
089    private Engine m_engine;
090
091    private static final Logger log = LogManager.getLogger( DefaultUserManager.class);
092
093    /** Associates wiki sessions with profiles */
094    private final Map< Session, UserProfile > m_profiles = new WeakHashMap<>();
095
096    /** The user database loads, manages and persists user identities */
097    private UserDatabase m_database;
098
099    /** {@inheritDoc} */
100    @Override
101    public void initialize( final Engine engine, final Properties props ) {
102        m_engine = engine;
103
104        // Attach the PageManager as a listener
105        // TODO: it would be better if we did this in PageManager directly
106        addWikiEventListener( engine.getManager( PageManager.class ) );
107
108        //TODO: Replace with custom annotations. See JSPWIKI-566
109        WikiAjaxDispatcherServlet.registerServlet( JSON_USERS, new JSONUserModule(this), new AllPermission(null));
110    }
111
112    /** {@inheritDoc} */
113    @Override
114    public UserDatabase getUserDatabase() {
115        if( m_database != null ) {
116            return m_database;
117        }
118
119        String dbClassName = UNKNOWN_CLASS;
120
121        try {
122            dbClassName = TextUtil.getRequiredProperty( m_engine.getWikiProperties(), PROP_DATABASE );
123
124            log.info( "Attempting to load user database class " + dbClassName );
125            final Class<?> dbClass = ClassUtil.findClass( USERDATABASE_PACKAGE, dbClassName );
126            m_database = (UserDatabase) dbClass.newInstance();
127            m_database.initialize( m_engine, m_engine.getWikiProperties() );
128            log.info("UserDatabase initialized.");
129        } catch( final NoSuchElementException | NoRequiredPropertyException e ) {
130            log.error( "You have not set the '"+PROP_DATABASE+"'. You need to do this if you want to enable user management by JSPWiki.", e );
131        } catch( final ClassNotFoundException e ) {
132            log.error( "UserDatabase class " + dbClassName + " cannot be found", e );
133        } catch( final InstantiationException e ) {
134            log.error( "UserDatabase class " + dbClassName + " cannot be created", e );
135        } catch( final IllegalAccessException e ) {
136            log.error( "You are not allowed to access this user database class", e );
137        } catch( final WikiSecurityException e ) {
138            log.error( "Exception initializing user database: " + e.getMessage(), e );
139        } finally {
140            if( m_database == null ) {
141                log.info("I could not create a database object you specified (or didn't specify), so I am falling back to a default.");
142                m_database = new DummyUserDatabase();
143            }
144        }
145
146        return m_database;
147    }
148
149    /** {@inheritDoc} */
150    @Override
151    public UserProfile getUserProfile( final Session session ) {
152        // Look up cached user profile
153        UserProfile profile = m_profiles.get( session );
154        boolean newProfile = profile == null;
155        Principal user = null;
156
157        // If user is authenticated, figure out if this is an existing profile
158        if ( session.isAuthenticated() ) {
159            user = session.getUserPrincipal();
160            try {
161                profile = getUserDatabase().find( user.getName() );
162                newProfile = false;
163            } catch( final NoSuchPrincipalException e ) { }
164        }
165
166        if ( newProfile ) {
167            profile = getUserDatabase().newProfile();
168            if ( user != null ) {
169                profile.setLoginName( user.getName() );
170            }
171            if ( !profile.isNew() ) {
172                throw new IllegalStateException( "New profile should be marked 'new'. Check your UserProfile implementation." );
173            }
174        }
175
176        // Stash the profile for next time
177        m_profiles.put( session, profile );
178        return profile;
179    }
180
181    /** {@inheritDoc} */
182    @Override
183    public void setUserProfile( final Context context, final UserProfile profile ) throws DuplicateUserException, WikiException {
184        final Session session = context.getWikiSession();
185        // Verify user is allowed to save profile!
186        final Permission p = new WikiPermission( m_engine.getApplicationName(), WikiPermission.EDIT_PROFILE_ACTION );
187        if ( !m_engine.getManager( AuthorizationManager.class ).checkPermission( session, p ) ) {
188            throw new WikiSecurityException( "You are not allowed to save wiki profiles." );
189        }
190
191        // Check if profile is new, and see if container allows creation
192        final boolean newProfile = profile.isNew();
193
194        // Check if another user profile already has the fullname or loginname
195        final UserProfile oldProfile = getUserProfile( session );
196        final boolean nameChanged = ( oldProfile != null && oldProfile.getFullname() != null ) &&
197                                    !( oldProfile.getFullname().equals( profile.getFullname() ) &&
198                                    oldProfile.getLoginName().equals( profile.getLoginName() ) );
199        UserProfile otherProfile;
200        try {
201            otherProfile = getUserDatabase().findByLoginName( profile.getLoginName() );
202            if( otherProfile != null && !otherProfile.equals( oldProfile ) ) {
203                throw new DuplicateUserException( "security.error.login.taken", profile.getLoginName() );
204            }
205        } catch( final NoSuchPrincipalException e ) {
206        }
207        try {
208            otherProfile = getUserDatabase().findByFullName( profile.getFullname() );
209            if( otherProfile != null && !otherProfile.equals( oldProfile ) ) {
210                throw new DuplicateUserException( "security.error.fullname.taken", profile.getFullname() );
211            }
212        } catch( final NoSuchPrincipalException e ) {
213        }
214
215        // For new accounts, create approval workflow for user profile save.
216        if( newProfile && oldProfile != null && oldProfile.isNew() ) {
217            startUserProfileCreationWorkflow( context, profile );
218
219            // If the profile doesn't need approval, then just log the user in
220
221            try {
222                final AuthenticationManager mgr = m_engine.getManager( AuthenticationManager.class );
223                if( !mgr.isContainerAuthenticated() ) {
224                    mgr.login( session, null, profile.getLoginName(), profile.getPassword() );
225                }
226            } catch( final WikiException e ) {
227                throw new WikiSecurityException( e.getMessage(), e );
228            }
229
230            // Alert all listeners that the profile changed...
231            // ...this will cause credentials to be reloaded in the wiki session
232            fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
233        } else { // For existing accounts, just save the profile
234            // If login name changed, rename it first
235            if( nameChanged && !oldProfile.getLoginName().equals( profile.getLoginName() ) ) {
236                getUserDatabase().rename( oldProfile.getLoginName(), profile.getLoginName() );
237            }
238
239            // Now, save the profile (userdatabase will take care of timestamps for us)
240            getUserDatabase().save( profile );
241
242            if( nameChanged ) {
243                // Fire an event if the login name or full name changed
244                final UserProfile[] profiles = new UserProfile[] { oldProfile, profile };
245                fireEvent( WikiSecurityEvent.PROFILE_NAME_CHANGED, session, profiles );
246            } else {
247                // Fire an event that says we have new a new profile (new principals)
248                fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
249            }
250        }
251    }
252
253    /** {@inheritDoc} */
254    @Override
255    public void startUserProfileCreationWorkflow( final Context context, final UserProfile profile ) throws WikiException {
256        final WorkflowBuilder builder = WorkflowBuilder.getBuilder( m_engine );
257        final Principal submitter = context.getWikiSession().getUserPrincipal();
258        final Step completionTask = m_engine.getManager( TasksManager.class ).buildSaveUserProfileTask( context.getWikiSession().getLocale() );
259
260        // Add user profile attribute as Facts for the approver (if required)
261        final boolean hasEmail = profile.getEmail() != null;
262        final Fact[] facts = new Fact[ hasEmail ? 4 : 3 ];
263        facts[ 0 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_FULL_NAME, profile.getFullname() );
264        facts[ 1 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_LOGIN_NAME, profile.getLoginName() );
265        facts[ 2 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_SUBMITTER, submitter.getName() );
266        if ( hasEmail ) {
267            facts[ 3 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_EMAIL, profile.getEmail() );
268        }
269        final Workflow workflow = builder.buildApprovalWorkflow( submitter,
270                                                                 WorkflowManager.WF_UP_CREATE_SAVE_APPROVER,
271                                                                 null,
272                                                                 WorkflowManager.WF_UP_CREATE_SAVE_DECISION_MESSAGE_KEY,
273                                                                 facts,
274                                                                 completionTask,
275                                                                 null );
276
277        workflow.setAttribute( WorkflowManager.WF_UP_CREATE_SAVE_ATTR_SAVED_PROFILE, profile );
278        workflow.start( context );
279
280        final boolean approvalRequired = workflow.getCurrentStep() instanceof Decision;
281
282        // If the profile requires approval, redirect user to message page
283        if ( approvalRequired ) {
284            throw new DecisionRequiredException( "This profile must be approved before it becomes active" );
285        }
286    }
287
288    /** {@inheritDoc} */
289    @Override
290    public UserProfile parseProfile( final Context context ) {
291        // Retrieve the user's profile (may have been previously cached)
292        final UserProfile profile = getUserProfile( context.getWikiSession() );
293        final HttpServletRequest request = context.getHttpRequest();
294
295        // Extract values from request stream (cleanse whitespace as needed)
296        String loginName = request.getParameter( PARAM_LOGINNAME );
297        String password = request.getParameter( PARAM_PASSWORD );
298        String fullname = request.getParameter( PARAM_FULLNAME );
299        String email = request.getParameter( PARAM_EMAIL );
300        loginName = InputValidator.isBlank( loginName ) ? null : loginName;
301        password = InputValidator.isBlank( password ) ? null : password;
302        fullname = InputValidator.isBlank( fullname ) ? null : fullname;
303        email = InputValidator.isBlank( email ) ? null : email;
304
305        // A special case if we have container authentication: if authenticated, login name is always taken from container
306        if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() && context.getWikiSession().isAuthenticated() ) {
307            loginName = context.getWikiSession().getLoginPrincipal().getName();
308        }
309
310        // Set the profile fields!
311        profile.setLoginName( loginName );
312        profile.setEmail( email );
313        profile.setFullname( fullname );
314        profile.setPassword( password );
315        return profile;
316    }
317
318    /** {@inheritDoc} */
319    @Override
320    public void validateProfile( final Context context, final UserProfile profile ) {
321        final boolean isNew = profile.isNew();
322        final Session session = context.getWikiSession();
323        final InputValidator validator = new InputValidator( SESSION_MESSAGES, context );
324        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
325
326        //  Query the SpamFilter first
327        final FilterManager fm = m_engine.getManager( FilterManager.class );
328        final List< PageFilter > ls = fm.getFilterList();
329        for( final PageFilter pf : ls ) {
330            if( pf instanceof SpamFilter ) {
331                if( !( ( SpamFilter )pf ).isValidUserProfile( context, profile ) ) {
332                    session.addMessage( SESSION_MESSAGES, "Invalid userprofile" );
333                    return;
334                }
335                break;
336            }
337        }
338
339        // If container-managed auth and user not logged in, throw an error
340        if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated()
341             && !context.getWikiSession().isAuthenticated() ) {
342            session.addMessage( SESSION_MESSAGES, rb.getString("security.error.createprofilebeforelogin") );
343        }
344
345        validator.validateNotNull( profile.getLoginName(), rb.getString("security.user.loginname") );
346        validator.validateNotNull( profile.getFullname(), rb.getString("security.user.fullname") );
347        validator.validate( profile.getEmail(), rb.getString("security.user.email"), InputValidator.EMAIL );
348
349        // If new profile, passwords must match and can't be null
350        if( !m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() ) {
351            final String password = profile.getPassword();
352            if( password == null ) {
353                if( isNew ) {
354                    session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.blankpassword" ) );
355                }
356            } else {
357                final HttpServletRequest request = context.getHttpRequest();
358                final String password2 = ( request == null ) ? null : request.getParameter( "password2" );
359                if( !password.equals( password2 ) ) {
360                    session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) );
361                }
362            }
363        }
364
365        UserProfile otherProfile;
366        final String fullName = profile.getFullname();
367        final String loginName = profile.getLoginName();
368        final String email = profile.getEmail();
369
370        // It's illegal to use as a full name someone else's login name
371        try {
372            otherProfile = getUserDatabase().find( fullName );
373            if( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) ) {
374                final Object[] args = { fullName };
375                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalfullname" ), args ) );
376            }
377        } catch( final NoSuchPrincipalException e ) { /* It's clean */ }
378
379        // It's illegal to use as a login name someone else's full name
380        try {
381            otherProfile = getUserDatabase().find( loginName );
382            if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) {
383                final Object[] args = { loginName };
384                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) );
385            }
386        } catch( final NoSuchPrincipalException e ) { /* It's clean */ }
387
388        // It's illegal to use multiple accounts with the same email
389        try {
390            otherProfile = getUserDatabase().findByEmail( email );
391            if( otherProfile != null && !profile.getUid().equals( otherProfile.getUid() ) // Issue JSPWIKI-1042
392                    && !profile.equals( otherProfile ) && StringUtils.lowerCase( email )
393                    .equals( StringUtils.lowerCase( otherProfile.getEmail() ) ) ) {
394                final Object[] args = { email };
395                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.email.taken" ), args ) );
396            }
397        } catch( final NoSuchPrincipalException e ) { /* It's clean */ }
398    }
399
400    /** {@inheritDoc} */
401    @Override
402    public Principal[] listWikiNames() throws WikiSecurityException {
403        return getUserDatabase().getWikiNames();
404    }
405
406    // events processing .......................................................
407
408    /**
409     * Registers a WikiEventListener with this instance.
410     * This is a convenience method.
411     * @param listener the event listener
412     */
413    @Override public synchronized void addWikiEventListener( final WikiEventListener listener ) {
414        WikiEventManager.addWikiEventListener( this, listener );
415    }
416
417    /**
418     * Un-registers a WikiEventListener with this instance.
419     * This is a convenience method.
420     * @param listener the event listener
421     */
422    @Override public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
423        WikiEventManager.removeWikiEventListener( this, listener );
424    }
425
426    /**
427     *  Implements the JSON API for usermanager.
428     *  <p>
429     *  Even though this gets serialized whenever container shuts down/restarts, this gets reinstalled to the session when JSPWiki starts.
430     *  This means that it's not actually necessary to save anything.
431     */
432    public static final class JSONUserModule implements WikiAjaxServlet {
433
434        private final DefaultUserManager m_manager;
435
436        /**
437         *  Create a new JSONUserModule.
438         *  @param mgr Manager
439         */
440        public JSONUserModule( final DefaultUserManager mgr )
441        {
442            m_manager = mgr;
443        }
444
445        @Override
446        public String getServletMapping() {
447            return JSON_USERS;
448        }
449
450        @Override
451        public void service( final HttpServletRequest req, final HttpServletResponse resp, final String actionName, final List<String> params) throws ServletException, IOException {
452            try {
453                if( params.size() < 1 ) {
454                    return;
455                }
456                final String uid = params.get(0);
457                log.debug("uid="+uid);
458                if (StringUtils.isNotBlank(uid)) {
459                    final UserProfile prof = getUserInfo(uid);
460                    resp.getWriter().write(AjaxUtil.toJson(prof));
461                }
462            } catch (final NoSuchPrincipalException e) {
463                throw new ServletException(e);
464            }
465        }
466
467        /**
468         *  Directly returns the UserProfile object attached to an uid.
469         *
470         *  @param uid The user id (e.g. WikiName)
471         *  @return A UserProfile object
472         *  @throws NoSuchPrincipalException If such a name does not exist.
473         */
474        public UserProfile getUserInfo( final String uid ) throws NoSuchPrincipalException {
475            if( m_manager != null ) {
476                return m_manager.getUserDatabase().find( uid );
477            }
478
479            throw new IllegalStateException( "The manager is offline." );
480        }
481    }
482
483}