blob: 776f741c550038ad8587909472f76a9dbc0116cb [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.wiki.auth;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.apache.wiki.ajax.AjaxUtil;
import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
import org.apache.wiki.ajax.WikiAjaxServlet;
import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.api.core.Session;
import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
import org.apache.wiki.api.exceptions.WikiException;
import org.apache.wiki.api.filters.PageFilter;
import org.apache.wiki.auth.permissions.AllPermission;
import org.apache.wiki.auth.permissions.WikiPermission;
import org.apache.wiki.auth.user.DummyUserDatabase;
import org.apache.wiki.auth.user.DuplicateUserException;
import org.apache.wiki.auth.user.UserDatabase;
import org.apache.wiki.auth.user.UserProfile;
import org.apache.wiki.event.WikiEventListener;
import org.apache.wiki.event.WikiEventManager;
import org.apache.wiki.event.WikiSecurityEvent;
import org.apache.wiki.filters.FilterManager;
import org.apache.wiki.filters.SpamFilter;
import org.apache.wiki.i18n.InternationalizationManager;
import org.apache.wiki.pages.PageManager;
import org.apache.wiki.preferences.Preferences;
import org.apache.wiki.tasks.TasksManager;
import org.apache.wiki.ui.InputValidator;
import org.apache.wiki.util.ClassUtil;
import org.apache.wiki.util.TextUtil;
import org.apache.wiki.workflow.Decision;
import org.apache.wiki.workflow.DecisionRequiredException;
import org.apache.wiki.workflow.Fact;
import org.apache.wiki.workflow.Step;
import org.apache.wiki.workflow.Workflow;
import org.apache.wiki.workflow.WorkflowBuilder;
import org.apache.wiki.workflow.WorkflowManager;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Permission;
import java.security.Principal;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.WeakHashMap;
/**
* Default implementation for {@link UserManager}.
*
* @since 2.3
*/
public class DefaultUserManager implements UserManager {
private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user";
private static final String SESSION_MESSAGES = "profile";
private static final String PARAM_EMAIL = "email";
private static final String PARAM_FULLNAME = "fullname";
private static final String PARAM_PASSWORD = "password";
private static final String PARAM_LOGINNAME = "loginname";
private static final String UNKNOWN_CLASS = "<unknown>";
private Engine m_engine;
private static final Logger log = Logger.getLogger( DefaultUserManager.class);
/** Associates wiki sessions with profiles */
private final Map< Session, UserProfile > m_profiles = new WeakHashMap<>();
/** The user database loads, manages and persists user identities */
private UserDatabase m_database;
/** {@inheritDoc} */
@Override
public void initialize( final Engine engine, final Properties props ) {
m_engine = engine;
// Attach the PageManager as a listener
// TODO: it would be better if we did this in PageManager directly
addWikiEventListener( engine.getManager( PageManager.class ) );
//TODO: Replace with custom annotations. See JSPWIKI-566
WikiAjaxDispatcherServlet.registerServlet( JSON_USERS, new JSONUserModule(this), new AllPermission(null));
}
/** {@inheritDoc} */
@Override
public UserDatabase getUserDatabase() {
if( m_database != null ) {
return m_database;
}
String dbClassName = UNKNOWN_CLASS;
try {
dbClassName = TextUtil.getRequiredProperty( m_engine.getWikiProperties(), PROP_DATABASE );
log.info( "Attempting to load user database class " + dbClassName );
final Class<?> dbClass = ClassUtil.findClass( USERDATABASE_PACKAGE, dbClassName );
m_database = (UserDatabase) dbClass.newInstance();
m_database.initialize( m_engine, m_engine.getWikiProperties() );
log.info("UserDatabase initialized.");
} catch( final NoSuchElementException | NoRequiredPropertyException e ) {
log.error( "You have not set the '"+PROP_DATABASE+"'. You need to do this if you want to enable user management by JSPWiki.", e );
} catch( final ClassNotFoundException e ) {
log.error( "UserDatabase class " + dbClassName + " cannot be found", e );
} catch( final InstantiationException e ) {
log.error( "UserDatabase class " + dbClassName + " cannot be created", e );
} catch( final IllegalAccessException e ) {
log.error( "You are not allowed to access this user database class", e );
} catch( final WikiSecurityException e ) {
log.error( "Exception initializing user database: " + e.getMessage(), e );
} finally {
if( m_database == null ) {
log.info("I could not create a database object you specified (or didn't specify), so I am falling back to a default.");
m_database = new DummyUserDatabase();
}
}
return m_database;
}
/** {@inheritDoc} */
@Override
public UserProfile getUserProfile( final Session session ) {
// Look up cached user profile
UserProfile profile = m_profiles.get( session );
boolean newProfile = profile == null;
Principal user = null;
// If user is authenticated, figure out if this is an existing profile
if ( session.isAuthenticated() ) {
user = session.getUserPrincipal();
try {
profile = getUserDatabase().find( user.getName() );
newProfile = false;
} catch( final NoSuchPrincipalException e ) { }
}
if ( newProfile ) {
profile = getUserDatabase().newProfile();
if ( user != null ) {
profile.setLoginName( user.getName() );
}
if ( !profile.isNew() ) {
throw new IllegalStateException( "New profile should be marked 'new'. Check your UserProfile implementation." );
}
}
// Stash the profile for next time
m_profiles.put( session, profile );
return profile;
}
/** {@inheritDoc} */
@Override
public void setUserProfile( final Session session, final UserProfile profile ) throws DuplicateUserException, WikiException {
// Verify user is allowed to save profile!
final Permission p = new WikiPermission( m_engine.getApplicationName(), WikiPermission.EDIT_PROFILE_ACTION );
if ( !m_engine.getManager( AuthorizationManager.class ).checkPermission( session, p ) ) {
throw new WikiSecurityException( "You are not allowed to save wiki profiles." );
}
// Check if profile is new, and see if container allows creation
final boolean newProfile = profile.isNew();
// Check if another user profile already has the fullname or loginname
final UserProfile oldProfile = getUserProfile( session );
final boolean nameChanged = ( oldProfile != null && oldProfile.getFullname() != null ) &&
!( oldProfile.getFullname().equals( profile.getFullname() ) &&
oldProfile.getLoginName().equals( profile.getLoginName() ) );
UserProfile otherProfile;
try {
otherProfile = getUserDatabase().findByLoginName( profile.getLoginName() );
if( otherProfile != null && !otherProfile.equals( oldProfile ) ) {
throw new DuplicateUserException( "security.error.login.taken", profile.getLoginName() );
}
} catch( final NoSuchPrincipalException e ) {
}
try {
otherProfile = getUserDatabase().findByFullName( profile.getFullname() );
if( otherProfile != null && !otherProfile.equals( oldProfile ) ) {
throw new DuplicateUserException( "security.error.fullname.taken", profile.getFullname() );
}
} catch( final NoSuchPrincipalException e ) {
}
// For new accounts, create approval workflow for user profile save.
if( newProfile && oldProfile != null && oldProfile.isNew() ) {
startUserProfileCreationWorkflow( session, profile );
// If the profile doesn't need approval, then just log the user in
try {
final AuthenticationManager mgr = m_engine.getManager( AuthenticationManager.class );
if( !mgr.isContainerAuthenticated() ) {
mgr.login( session, null, profile.getLoginName(), profile.getPassword() );
}
} catch( final WikiException e ) {
throw new WikiSecurityException( e.getMessage(), e );
}
// Alert all listeners that the profile changed...
// ...this will cause credentials to be reloaded in the wiki session
fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
} else { // For existing accounts, just save the profile
// If login name changed, rename it first
if( nameChanged && !oldProfile.getLoginName().equals( profile.getLoginName() ) ) {
getUserDatabase().rename( oldProfile.getLoginName(), profile.getLoginName() );
}
// Now, save the profile (userdatabase will take care of timestamps for us)
getUserDatabase().save( profile );
if( nameChanged ) {
// Fire an event if the login name or full name changed
final UserProfile[] profiles = new UserProfile[] { oldProfile, profile };
fireEvent( WikiSecurityEvent.PROFILE_NAME_CHANGED, session, profiles );
} else {
// Fire an event that says we have new a new profile (new principals)
fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
}
}
}
/** {@inheritDoc} */
@Override
public void startUserProfileCreationWorkflow( final Session session, final UserProfile profile ) throws WikiException {
final WorkflowBuilder builder = WorkflowBuilder.getBuilder( m_engine );
final Principal submitter = session.getUserPrincipal();
final Step completionTask = m_engine.getManager( TasksManager.class ).buildSaveUserProfileTask( m_engine, session.getLocale() );
// Add user profile attribute as Facts for the approver (if required)
final boolean hasEmail = profile.getEmail() != null;
final Fact[] facts = new Fact[ hasEmail ? 4 : 3 ];
facts[ 0 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_FULL_NAME, profile.getFullname() );
facts[ 1 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_LOGIN_NAME, profile.getLoginName() );
facts[ 2 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_SUBMITTER, submitter.getName() );
if ( hasEmail ) {
facts[ 3 ] = new Fact( WorkflowManager.WF_UP_CREATE_SAVE_FACT_PREFS_EMAIL, profile.getEmail() );
}
final Workflow workflow = builder.buildApprovalWorkflow( submitter,
WorkflowManager.WF_UP_CREATE_SAVE_APPROVER,
null,
WorkflowManager.WF_UP_CREATE_SAVE_DECISION_MESSAGE_KEY,
facts,
completionTask,
null );
workflow.setAttribute( WorkflowManager.WF_UP_CREATE_SAVE_ATTR_SAVED_PROFILE, profile );
m_engine.getManager( WorkflowManager.class ).start( workflow );
final boolean approvalRequired = workflow.getCurrentStep() instanceof Decision;
// If the profile requires approval, redirect user to message page
if ( approvalRequired ) {
throw new DecisionRequiredException( "This profile must be approved before it becomes active" );
}
}
/** {@inheritDoc} */
@Override
public UserProfile parseProfile( final Context context ) {
// Retrieve the user's profile (may have been previously cached)
final UserProfile profile = getUserProfile( context.getWikiSession() );
final HttpServletRequest request = context.getHttpRequest();
// Extract values from request stream (cleanse whitespace as needed)
String loginName = request.getParameter( PARAM_LOGINNAME );
String password = request.getParameter( PARAM_PASSWORD );
String fullname = request.getParameter( PARAM_FULLNAME );
String email = request.getParameter( PARAM_EMAIL );
loginName = InputValidator.isBlank( loginName ) ? null : loginName;
password = InputValidator.isBlank( password ) ? null : password;
fullname = InputValidator.isBlank( fullname ) ? null : fullname;
email = InputValidator.isBlank( email ) ? null : email;
// A special case if we have container authentication: if authenticated, login name is always taken from container
if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() && context.getWikiSession().isAuthenticated() ) {
loginName = context.getWikiSession().getLoginPrincipal().getName();
}
// Set the profile fields!
profile.setLoginName( loginName );
profile.setEmail( email );
profile.setFullname( fullname );
profile.setPassword( password );
return profile;
}
/** {@inheritDoc} */
@Override
public void validateProfile( final Context context, final UserProfile profile ) {
final boolean isNew = profile.isNew();
final Session session = context.getWikiSession();
final InputValidator validator = new InputValidator( SESSION_MESSAGES, context );
final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
// Query the SpamFilter first
final FilterManager fm = m_engine.getManager( FilterManager.class );
final List< PageFilter > ls = fm.getFilterList();
for( final PageFilter pf : ls ) {
if( pf instanceof SpamFilter ) {
if( !( ( SpamFilter )pf ).isValidUserProfile( context, profile ) ) {
session.addMessage( SESSION_MESSAGES, "Invalid userprofile" );
return;
}
break;
}
}
// If container-managed auth and user not logged in, throw an error
if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated()
&& !context.getWikiSession().isAuthenticated() ) {
session.addMessage( SESSION_MESSAGES, rb.getString("security.error.createprofilebeforelogin") );
}
validator.validateNotNull( profile.getLoginName(), rb.getString("security.user.loginname") );
validator.validateNotNull( profile.getFullname(), rb.getString("security.user.fullname") );
validator.validate( profile.getEmail(), rb.getString("security.user.email"), InputValidator.EMAIL );
// If new profile, passwords must match and can't be null
if( !m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() ) {
final String password = profile.getPassword();
if( password == null ) {
if( isNew ) {
session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.blankpassword" ) );
}
} else {
final HttpServletRequest request = context.getHttpRequest();
final String password2 = ( request == null ) ? null : request.getParameter( "password2" );
if( !password.equals( password2 ) ) {
session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) );
}
}
}
UserProfile otherProfile;
final String fullName = profile.getFullname();
final String loginName = profile.getLoginName();
final String email = profile.getEmail();
// It's illegal to use as a full name someone else's login name
try {
otherProfile = getUserDatabase().find( fullName );
if( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) ) {
final Object[] args = { fullName };
session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalfullname" ), args ) );
}
} catch( final NoSuchPrincipalException e ) { /* It's clean */ }
// It's illegal to use as a login name someone else's full name
try {
otherProfile = getUserDatabase().find( loginName );
if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) {
final Object[] args = { loginName };
session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) );
}
} catch( final NoSuchPrincipalException e ) { /* It's clean */ }
// It's illegal to use multiple accounts with the same email
try {
otherProfile = getUserDatabase().findByEmail( email );
if( otherProfile != null && !profile.getUid().equals( otherProfile.getUid() ) // Issue JSPWIKI-1042
&& !profile.equals( otherProfile ) && StringUtils.lowerCase( email )
.equals( StringUtils.lowerCase( otherProfile.getEmail() ) ) ) {
final Object[] args = { email };
session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.email.taken" ), args ) );
}
} catch( final NoSuchPrincipalException e ) { /* It's clean */ }
}
/** {@inheritDoc} */
@Override
public Principal[] listWikiNames() throws WikiSecurityException {
return getUserDatabase().getWikiNames();
}
// events processing .......................................................
/**
* Registers a WikiEventListener with this instance.
* This is a convenience method.
* @param listener the event listener
*/
@Override public synchronized void addWikiEventListener( final WikiEventListener listener ) {
WikiEventManager.addWikiEventListener( this, listener );
}
/**
* Un-registers a WikiEventListener with this instance.
* This is a convenience method.
* @param listener the event listener
*/
@Override public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
WikiEventManager.removeWikiEventListener( this, listener );
}
/**
* Implements the JSON API for usermanager.
* <p>
* Even though this gets serialized whenever container shuts down/restarts, this gets reinstalled to the session when JSPWiki starts.
* This means that it's not actually necessary to save anything.
*/
public static final class JSONUserModule implements WikiAjaxServlet {
private volatile DefaultUserManager m_manager;
/**
* Create a new JSONUserModule.
* @param mgr Manager
*/
public JSONUserModule( final DefaultUserManager mgr )
{
m_manager = mgr;
}
@Override
public String getServletMapping() {
return JSON_USERS;
}
@Override
public void service( final HttpServletRequest req, final HttpServletResponse resp, final String actionName, final List<String> params) throws ServletException, IOException {
try {
if( params.size() < 1 ) {
return;
}
final String uid = params.get(0);
log.debug("uid="+uid);
if (StringUtils.isNotBlank(uid)) {
final UserProfile prof = getUserInfo(uid);
resp.getWriter().write(AjaxUtil.toJson(prof));
}
} catch (final NoSuchPrincipalException e) {
throw new ServletException(e);
}
}
/**
* Directly returns the UserProfile object attached to an uid.
*
* @param uid The user id (e.g. WikiName)
* @return A UserProfile object
* @throws NoSuchPrincipalException If such a name does not exist.
*/
public UserProfile getUserInfo( final String uid ) throws NoSuchPrincipalException {
if( m_manager != null ) {
return m_manager.getUserDatabase().find( uid );
}
throw new IllegalStateException( "The manager is offline." );
}
}
}