001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.usermanagement; 017 018import java.util.HashMap; 019import java.util.List; 020import java.util.Map; 021import java.util.Optional; 022 023import org.apache.avalon.framework.parameters.Parameters; 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.cocoon.acting.ServiceableAction; 027import org.apache.cocoon.environment.ObjectModelHelper; 028import org.apache.cocoon.environment.Redirector; 029import org.apache.cocoon.environment.Request; 030import org.apache.cocoon.environment.SourceResolver; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.core.user.CurrentUserProvider; 034import org.ametys.core.user.UserIdentity; 035import org.ametys.runtime.authentication.AuthorizationRequiredException; 036import org.ametys.runtime.i18n.I18nizableText; 037import org.ametys.web.renderingcontext.RenderingContext; 038import org.ametys.web.renderingcontext.RenderingContextHandler; 039import org.ametys.web.usermanagement.UserManagementException.StatusError; 040 041import com.google.common.collect.ArrayListMultimap; 042import com.google.common.collect.Multimap; 043 044/** 045 * Handle the lost password and change password actions. 046 */ 047public class UserPasswordAction extends ServiceableAction 048{ 049 /** The user signup manager. */ 050 protected UserSignupManager _userSignupManager; 051 052 /** The rendering context handler. */ 053 protected RenderingContextHandler _renderingContextHandler; 054 055 /** The current user provider */ 056 protected CurrentUserProvider _currentUserProvider; 057 058 @Override 059 public void service(ServiceManager serviceManager) throws ServiceException 060 { 061 super.service(serviceManager); 062 _userSignupManager = (UserSignupManager) serviceManager.lookup(UserSignupManager.ROLE); 063 _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE); 064 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 065 } 066 067 @Override 068 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 069 { 070 Request request = ObjectModelHelper.getRequest(objectModel); 071 String siteName = (String) request.getAttribute("site"); 072 String language = (String) request.getAttribute("sitemapLanguage"); 073 RenderingContext renderingContext = _renderingContextHandler.getRenderingContext(); 074 075 Map<String, Object> results = new HashMap<>(); 076 077 UserIdentity foUser = _currentUserProvider.getUser(); 078 079 boolean lostPassword = "true".equals(request.getParameter("lost-password")); // (1) when unconnected user ask for a new password 080 boolean changePassword = "true".equals(request.getParameter("change-password")); // (2) when connected user ask for change password 081 boolean submitPassword = "true".equals(request.getParameter("pwd-submit")); // (3) when connected or unconnected user submit a new password 082 083 084 String mode = request.getParameter("mode"); 085 String login = request.getParameter("login"); 086 String population = request.getParameter("population"); 087 String token = request.getParameter("token"); 088 089 boolean reinitPassword = StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(login) && StringUtils.isNotEmpty(population); 090 091 Multimap<String, I18nizableText> errors = ArrayListMultimap.create(); 092 093 try 094 { 095 if ("lostpassword".equals(mode)) 096 { 097 _lostPasswordMode(results); 098 } 099 else if (lostPassword) 100 { 101 _newPasswordRequest(results, request, siteName, language, errors); 102 } 103 else if (changePassword) 104 { 105 _changePasswordRequest(results, request, siteName, language, errors); 106 } 107 else if (submitPassword) 108 { 109 _submitNewPassword(results, request, siteName, token, errors); 110 } 111 else if (reinitPassword) 112 { 113 _reinitPassword(results, request, siteName, login, token, population, errors); 114 } 115 else if (foUser != null) 116 { 117 // Default behavior: if user is connected, display the form to request a new token with the connected user. 118 results.put("step", "change-password"); 119 } 120 else if (renderingContext == RenderingContext.FRONT) 121 { 122 // Send a 401 to force the user to authenticate. 123 throw new AuthorizationRequiredException(); 124 } 125 } 126 catch (UserManagementException e) 127 { 128 errors.put("global", new I18nizableText("general-error")); 129 results.put("step", "no-form"); 130 131 getLogger().error("An error occurred resetting a user password.", e); 132 } 133 134 request.setAttribute("errors", errors); 135 136 return results; 137 } 138 139 private void _lostPasswordMode(Map<String, Object> results) 140 { 141 // Unconnected user clicked on "lost password" => display the form to request a new token. 142 results.put("step", "lost-password"); 143 } 144 145 private void _newPasswordRequest(Map<String, Object> results, Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 146 { 147 // Unconnected user entered his email/login and population to request a new token by email 148 results.put("step", "lost-password-change"); 149 resetPassword(request, siteName, language, errors); 150 if (errors.isEmpty()) 151 { 152 results.put("status", "success"); 153 } 154 } 155 156 private void _changePasswordRequest(Map<String, Object> results, Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 157 { 158 // Connected user asks for a new token by email 159 results.put("step", "change-password-change"); 160 resetConnectedUserPassword(request, siteName, language, errors); 161 if (errors.isEmpty()) 162 { 163 results.put("status", "success"); 164 } 165 } 166 167 private void _submitNewPassword(Map<String, Object> results, Request request, String siteName, String token, Multimap<String, I18nizableText> errors) throws UserManagementException 168 { 169 // Connected or unconnected user submitted a new password 170 results.put("step", "user-update"); 171 changeUserPassword(request, siteName, token, errors); 172 if (errors.isEmpty()) 173 { 174 results.put("status", "success"); 175 } 176 } 177 178 private void _reinitPassword(Map<String, Object> results, Request request, String siteName, String login, String token, String population, Multimap<String, I18nizableText> errors) throws UserManagementException 179 { 180 // User clicked on email link to display the password form. 181 // If a token is provided, check it. 182 results.put("step", "password"); 183 184 boolean weakPassword = "true".equals(request.getParameter("weak-password")); // (4) when unconnected user with a weak password has to define to new password 185 if (weakPassword) 186 { 187 // User with a weak password is forced to change its password. A token has been automatically generated. 188 results.put("weak-password", "true"); 189 } 190 checkPasswordToken(request, siteName, login, token, population, errors); 191 if (errors.isEmpty()) 192 { 193 results.put("status", "success"); 194 } 195 } 196 /** 197 * Reset a user's sign-up request. 198 * @param request the user request. 199 * @param siteName the site name. 200 * @param language the language. 201 * @param errors the Map to fill with errors to display to the user. 202 * @throws UserManagementException if an error occurs. 203 */ 204 protected void resetPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 205 { 206 String email = request.getParameter("email"); 207 String populationId = request.getParameter("population"); 208 209 try 210 { 211 _userSignupManager.resetPassword(siteName, language, email, populationId); 212 } 213 catch (UserManagementException e) 214 { 215 StatusError statusError = e.getStatusError(); 216 switch (statusError) 217 { 218 case POPULATION_UNKNOWN: 219 case USER_UNKNOWN: 220 case UNMODIFIABLE_USER_DIRECTORY: 221 case NOT_UNIQUE_USER: 222 case EMPTY_EMAIL: 223 setGlobalError(statusError, errors, email, populationId); 224 break; 225 default: 226 throw e; 227 } 228 } 229 } 230 231 /** 232 * Reset a connected user password. 233 * @param request the user request. 234 * @param siteName the site name. 235 * @param language the language. 236 * @param errors the Map to fill with errors to display to the user. 237 * @throws UserManagementException if an error occurs. 238 */ 239 protected void resetConnectedUserPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 240 { 241 UserIdentity foUser = _currentUserProvider.getUser(); 242 243 if (foUser == null) 244 { 245 setGlobalError(StatusError.NOT_CONNECTED, errors, null, null); 246 } 247 else 248 { 249 String login = foUser.getLogin(); 250 String populationId = foUser.getPopulationId(); 251 252 try 253 { 254 _userSignupManager.resetPassword(siteName, language, login, populationId); 255 } 256 catch (UserManagementException e) 257 { 258 StatusError statusError = e.getStatusError(); 259 switch (statusError) 260 { 261 case POPULATION_UNKNOWN: 262 case USER_UNKNOWN: 263 case UNMODIFIABLE_USER_DIRECTORY: 264 case NOT_UNIQUE_USER: 265 case EMPTY_EMAIL: 266 setGlobalError(statusError, errors, login, populationId); 267 break; 268 default: 269 throw e; 270 } 271 } 272 } 273 } 274 275 /** 276 * Check that a token is valid. 277 * @param request the user request. 278 * @param siteName the site name. 279 * @param login the user login. 280 * @param token the sign-up token that was sent to the user. 281 * @param populationId The id of the population 282 * @param errors the Map to fill with errors to display to the user. 283 * @throws UserManagementException if an error occurs. 284 */ 285 protected void checkPasswordToken(Request request, String siteName, String login, String token, String populationId, Multimap<String, I18nizableText> errors) throws UserManagementException 286 { 287 try 288 { 289 _userSignupManager.checkPasswordToken(siteName, login, token, populationId); 290 } 291 catch (UserManagementException e) 292 { 293 StatusError statusError = e.getStatusError(); 294 switch (statusError) 295 { 296 case TOKEN_UNKNOWN: 297 case TOKEN_EXPIRED: 298 setGlobalError(statusError, errors, login, populationId); 299 break; 300 default: 301 throw e; 302 } 303 } 304 305 306 } 307 308 /** 309 * Sign-up the user: create a real user from his temporary information. 310 * @param request the user request. 311 * @param siteName the site name. 312 * @param token the sign-up token that was sent to the user. 313 * @param errors the Map to fill with errors to display to the user. 314 * @throws UserManagementException if an error occurs. 315 */ 316 protected void changeUserPassword(Request request, String siteName, String token, Multimap<String, I18nizableText> errors) throws UserManagementException 317 { 318 // Get the login either from the request or from the connected user. 319 String login = request.getParameter("login"); 320 String population = request.getParameter("population"); 321 UserIdentity foUser; 322 if (StringUtils.isEmpty(login) || StringUtils.isEmpty(population)) 323 { 324 foUser = _currentUserProvider.getUser(); 325 } 326 else 327 { 328 foUser = new UserIdentity(login, population); 329 } 330 331 if (foUser == null) 332 { 333 setGlobalError(StatusError.NOT_CONNECTED, errors, null, null); 334 return; 335 } 336 337 String password = request.getParameter("password"); 338 String passwordConfirmation = request.getParameter("password-confirmation"); 339 340 // First validation. 341 if (StringUtils.isBlank(password)) 342 { 343 errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY")); 344 } 345 else if (!password.equals(passwordConfirmation)) 346 { 347 errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH")); 348 } 349 350 // Full validation. 351 List<I18nizableText> inputErrors = _userSignupManager.validatePassword(siteName, password, foUser.getLogin(), foUser.getPopulationId()); 352 errors.putAll("password", inputErrors); 353 354 if (errors.isEmpty()) 355 { 356 try 357 { 358 // Validation passed: effectively change the password. 359 _userSignupManager.changeUserPassword(siteName, foUser.getLogin(), token, password, foUser.getPopulationId()); 360 } 361 catch (UserManagementException e) 362 { 363 StatusError statusError = e.getStatusError(); 364 switch (statusError) 365 { 366 case TOKEN_UNKNOWN: 367 case TOKEN_EXPIRED: 368 setGlobalError(statusError, errors, foUser.getLogin(), foUser.getPopulationId()); 369 break; 370 default: 371 throw e; 372 } 373 } 374 } 375 } 376 377 /** 378 * Set the global error if there is one. 379 * @param error The error to add (can be null) 380 * @param errors The errors map 381 * @param login The login of the user (can be null) 382 * @param populationId The population of the user (can be null) 383 */ 384 protected void setGlobalError(StatusError error, Multimap<String, I18nizableText> errors, String login, String populationId) 385 { 386 if (error != null) 387 { 388 errors.put("global", new I18nizableText(error.name())); 389 if (getLogger().isWarnEnabled()) 390 { 391 String message = String.format( 392 "Error during resetting the password of the connected user '%s' of population '%s': %s", 393 Optional.ofNullable(login).orElse(StringUtils.EMPTY), 394 Optional.ofNullable(populationId).orElse(StringUtils.EMPTY), 395 error.name()); 396 getLogger().warn(message); 397 } 398 } 399 } 400}