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}