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 = InputValidator.isBlank( loginName ) ? null : loginName; 296 password = InputValidator.isBlank( password ) ? null : password; 297 fullname = InputValidator.isBlank( fullname ) ? null : fullname; 298 email = InputValidator.isBlank( email ) ? null : 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 boolean isNew = profile.isNew(); 317 final Session session = context.getWikiSession(); 318 final InputValidator validator = new InputValidator( SESSION_MESSAGES, context ); 319 final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE ); 320 321 // Query the SpamFilter first 322 final FilterManager fm = m_engine.getManager( FilterManager.class ); 323 final List< PageFilter > ls = fm.getFilterList(); 324 for( final PageFilter pf : ls ) { 325 if( pf instanceof SpamFilter ) { 326 if( !( ( SpamFilter )pf ).isValidUserProfile( context, profile ) ) { 327 session.addMessage( SESSION_MESSAGES, "Invalid userprofile" ); 328 return; 329 } 330 break; 331 } 332 } 333 334 // If container-managed auth and user not logged in, throw an error 335 if ( m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() 336 && !context.getWikiSession().isAuthenticated() ) { 337 session.addMessage( SESSION_MESSAGES, rb.getString("security.error.createprofilebeforelogin") ); 338 } 339 340 validator.validateNotNull( profile.getLoginName(), rb.getString("security.user.loginname") ); 341 validator.validateNotNull( profile.getFullname(), rb.getString("security.user.fullname") ); 342 validator.validate( profile.getEmail(), rb.getString("security.user.email"), InputValidator.EMAIL ); 343 344 // If new profile, passwords must match and can't be null 345 if( !m_engine.getManager( AuthenticationManager.class ).isContainerAuthenticated() ) { 346 final String password = profile.getPassword(); 347 if( password == null ) { 348 if( isNew ) { 349 session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.blankpassword" ) ); 350 } 351 } else { 352 final HttpServletRequest request = context.getHttpRequest(); 353 final String password2 = ( request == null ) ? null : request.getParameter( "password2" ); 354 if( !password.equals( password2 ) ) { 355 session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) ); 356 } 357 } 358 } 359 360 UserProfile otherProfile; 361 final String fullName = profile.getFullname(); 362 final String loginName = profile.getLoginName(); 363 final String email = profile.getEmail(); 364 365 // It's illegal to use as a full name someone else's login name 366 try { 367 otherProfile = getUserDatabase().find( fullName ); 368 if( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) ) { 369 final Object[] args = { fullName }; 370 session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalfullname" ), args ) ); 371 } 372 } catch( final NoSuchPrincipalException e ) { /* It's clean */ } 373 374 // It's illegal to use as a login name someone else's full name 375 try { 376 otherProfile = getUserDatabase().find( loginName ); 377 if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) { 378 final Object[] args = { loginName }; 379 session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) ); 380 } 381 } catch( final NoSuchPrincipalException e ) { /* It's clean */ } 382 383 // It's illegal to use multiple accounts with the same email 384 try { 385 otherProfile = getUserDatabase().findByEmail( email ); 386 if( otherProfile != null && !profile.getUid().equals( otherProfile.getUid() ) // Issue JSPWIKI-1042 387 && !profile.equals( otherProfile ) && StringUtils.lowerCase( email ) 388 .equals( StringUtils.lowerCase( otherProfile.getEmail() ) ) ) { 389 final Object[] args = { email }; 390 session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.email.taken" ), args ) ); 391 } 392 } catch( final NoSuchPrincipalException e ) { /* It's clean */ } 393 } 394 395 /** {@inheritDoc} */ 396 @Override 397 public Principal[] listWikiNames() throws WikiSecurityException { 398 return getUserDatabase().getWikiNames(); 399 } 400 401 // events processing ....................................................... 402 403 /** 404 * Registers a WikiEventListener with this instance. 405 * This is a convenience method. 406 * @param listener the event listener 407 */ 408 @Override public synchronized void addWikiEventListener( final WikiEventListener listener ) { 409 WikiEventManager.addWikiEventListener( this, listener ); 410 } 411 412 /** 413 * Un-registers a WikiEventListener with this instance. 414 * This is a convenience method. 415 * @param listener the event listener 416 */ 417 @Override public synchronized void removeWikiEventListener( final WikiEventListener listener ) { 418 WikiEventManager.removeWikiEventListener( this, listener ); 419 } 420 421 /** 422 * Implements the JSON API for usermanager. 423 * <p> 424 * Even though this gets serialized whenever container shuts down/restarts, this gets reinstalled to the session when JSPWiki starts. 425 * This means that it's not actually necessary to save anything. 426 */ 427 public static final class JSONUserModule implements WikiAjaxServlet { 428 429 private final DefaultUserManager m_manager; 430 431 /** 432 * Create a new JSONUserModule. 433 * @param mgr Manager 434 */ 435 public JSONUserModule( final DefaultUserManager mgr ) 436 { 437 m_manager = mgr; 438 } 439 440 @Override 441 public String getServletMapping() { 442 return JSON_USERS; 443 } 444 445 @Override 446 public void service( final HttpServletRequest req, final HttpServletResponse resp, final String actionName, final List<String> params) throws ServletException, IOException { 447 try { 448 if( params.size() < 1 ) { 449 return; 450 } 451 final String uid = params.get(0); 452 log.debug("uid="+uid); 453 if (StringUtils.isNotBlank(uid)) { 454 final UserProfile prof = getUserInfo(uid); 455 resp.getWriter().write(AjaxUtil.toJson(prof)); 456 } 457 } catch (final NoSuchPrincipalException e) { 458 throw new ServletException(e); 459 } 460 } 461 462 /** 463 * Directly returns the UserProfile object attached to an uid. 464 * 465 * @param uid The user id (e.g. WikiName) 466 * @return A UserProfile object 467 * @throws NoSuchPrincipalException If such a name does not exist. 468 */ 469 public UserProfile getUserInfo( final String uid ) throws NoSuchPrincipalException { 470 if( m_manager != null ) { 471 return m_manager.getUserDatabase().find( uid ); 472 } 473 474 throw new IllegalStateException( "The manager is offline." ); 475 } 476 } 477 478}