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