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