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