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