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