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.Map; 020import java.util.Optional; 021 022import org.apache.avalon.framework.parameters.Parameters; 023import org.apache.avalon.framework.service.ServiceException; 024import org.apache.avalon.framework.service.ServiceManager; 025import org.apache.cocoon.acting.ServiceableAction; 026import org.apache.cocoon.environment.ObjectModelHelper; 027import org.apache.cocoon.environment.Redirector; 028import org.apache.cocoon.environment.Request; 029import org.apache.cocoon.environment.SourceResolver; 030import org.apache.commons.lang.StringUtils; 031 032import org.ametys.core.user.CurrentUserProvider; 033import org.ametys.core.user.UserIdentity; 034import org.ametys.runtime.authentication.AuthorizationRequiredException; 035import org.ametys.runtime.i18n.I18nizableText; 036import org.ametys.runtime.parameter.Errors; 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, String> 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 boolean weakPassword = "true".equals(request.getParameter("weak-password")); // (4) when unconnected user with a weak password has to define to new password 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 // Unconnected user clicked on "lost password" => display the form to request a new token. 098 results.put("step", "lost-password"); 099 } 100 else if (lostPassword) 101 { 102 // Unconnected user entered his email/login and population to request a new token by email 103 results.put("step", "lost-password-change"); 104 resetPassword(request, siteName, language, errors); 105 if (errors.isEmpty()) 106 { 107 results.put("status", "success"); 108 } 109 } 110 else if (changePassword) 111 { 112 // Connected user asks for a new token by email 113 results.put("step", "change-password-change"); 114 resetConnectedUserPassword(request, siteName, language, errors); 115 if (errors.isEmpty()) 116 { 117 results.put("status", "success"); 118 } 119 } 120 else if (submitPassword) 121 { 122 // Connected or unconnected user submitted a new password 123 results.put("step", "user-update"); 124 changeUserPassword(request, siteName, token, errors); 125 if (errors.isEmpty()) 126 { 127 results.put("status", "success"); 128 } 129 } 130 else if (reinitPassword) 131 { 132 // User clicked on email link to display the password form. 133 // If a token is provided, check it. 134 results.put("step", "password"); 135 if (weakPassword) 136 { 137 // User with a weak password is forced to change its password. A token has been automatically generated. 138 results.put("weak-password", "true"); 139 } 140 checkPasswordToken(request, siteName, login, token, population, errors); 141 if (errors.isEmpty()) 142 { 143 results.put("status", "success"); 144 } 145 } 146 else if (foUser != null) 147 { 148 // Default behavior: if user is connected, display the form to request a new token with the connected user. 149 results.put("step", "change-password"); 150 } 151 else if (renderingContext == RenderingContext.FRONT) 152 { 153 // Send a 401 to force the user to authenticate. 154 throw new AuthorizationRequiredException(); 155 } 156 } 157 catch (UserManagementException e) 158 { 159 errors.put("global", new I18nizableText("general-error")); 160 results.put("step", "no-form"); 161 162 getLogger().error("An error occurred resetting a user password.", e); 163 } 164 165 request.setAttribute("errors", errors); 166 167 return results; 168 } 169 170 /** 171 * Reset a user's sign-up request. 172 * @param request the user request. 173 * @param siteName the site name. 174 * @param language the language. 175 * @param errors the Map to fill with errors to display to the user. 176 * @throws UserManagementException if an error occurs. 177 */ 178 protected void resetPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 179 { 180 String email = request.getParameter("email"); 181 String populationId = request.getParameter("population"); 182 183 try 184 { 185 _userSignupManager.resetPassword(siteName, language, email, populationId); 186 } 187 catch (UserManagementException e) 188 { 189 StatusError statusError = e.getStatusError(); 190 switch (statusError) 191 { 192 case POPULATION_UNKNOWN: 193 case USER_UNKNOWN: 194 case UNMODIFIABLE_USER_DIRECTORY: 195 case NOT_UNIQUE_USER: 196 case EMPTY_EMAIL: 197 setGlobalError(statusError, errors, email, populationId); 198 break; 199 default: 200 throw e; 201 } 202 } 203 } 204 205 /** 206 * Reset a connected user password. 207 * @param request the user request. 208 * @param siteName the site name. 209 * @param language the language. 210 * @param errors the Map to fill with errors to display to the user. 211 * @throws UserManagementException if an error occurs. 212 */ 213 protected void resetConnectedUserPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 214 { 215 UserIdentity foUser = _currentUserProvider.getUser(); 216 217 if (foUser == null) 218 { 219 setGlobalError(StatusError.NOT_CONNECTED, errors, null, null); 220 } 221 else 222 { 223 String login = foUser.getLogin(); 224 String populationId = foUser.getPopulationId(); 225 226 try 227 { 228 _userSignupManager.resetPassword(siteName, language, login, populationId); 229 } 230 catch (UserManagementException e) 231 { 232 StatusError statusError = e.getStatusError(); 233 switch (statusError) 234 { 235 case POPULATION_UNKNOWN: 236 case USER_UNKNOWN: 237 case UNMODIFIABLE_USER_DIRECTORY: 238 case NOT_UNIQUE_USER: 239 case EMPTY_EMAIL: 240 setGlobalError(statusError, errors, login, populationId); 241 break; 242 default: 243 throw e; 244 } 245 } 246 } 247 } 248 249 /** 250 * Check that a token is valid. 251 * @param request the user request. 252 * @param siteName the site name. 253 * @param login the user login. 254 * @param token the sign-up token that was sent to the user. 255 * @param populationId The id of the population 256 * @param errors the Map to fill with errors to display to the user. 257 * @throws UserManagementException if an error occurs. 258 */ 259 protected void checkPasswordToken(Request request, String siteName, String login, String token, String populationId, Multimap<String, I18nizableText> errors) throws UserManagementException 260 { 261 try 262 { 263 _userSignupManager.checkPasswordToken(siteName, login, token, populationId); 264 } 265 catch (UserManagementException e) 266 { 267 StatusError statusError = e.getStatusError(); 268 switch (statusError) 269 { 270 case TOKEN_UNKNOWN: 271 case TOKEN_EXPIRED: 272 setGlobalError(statusError, errors, login, populationId); 273 break; 274 default: 275 throw e; 276 } 277 } 278 279 280 } 281 282 /** 283 * Sign-up the user: create a real user from his temporary information. 284 * @param request the user request. 285 * @param siteName the site name. 286 * @param token the sign-up token that was sent to the user. 287 * @param errors the Map to fill with errors to display to the user. 288 * @throws UserManagementException if an error occurs. 289 */ 290 protected void changeUserPassword(Request request, String siteName, String token, Multimap<String, I18nizableText> errors) throws UserManagementException 291 { 292 // Get the login either from the request or from the connected user. 293 String login = request.getParameter("login"); 294 String population = request.getParameter("population"); 295 UserIdentity foUser; 296 if (StringUtils.isEmpty(login) || StringUtils.isEmpty(population)) 297 { 298 foUser = _currentUserProvider.getUser(); 299 } 300 else 301 { 302 foUser = new UserIdentity(login, population); 303 } 304 305 if (foUser == null) 306 { 307 setGlobalError(StatusError.NOT_CONNECTED, errors, null, null); 308 return; 309 } 310 311 String password = request.getParameter("password"); 312 String passwordConfirmation = request.getParameter("password-confirmation"); 313 314 // First validation. 315 if (StringUtils.isBlank(password)) 316 { 317 errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY")); 318 } 319 else if (!password.equals(passwordConfirmation)) 320 { 321 errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH")); 322 } 323 324 // Full validation. 325 Map<String, Errors> inputErrors = _userSignupManager.validatePassword(siteName, password, foUser.getLogin(), foUser.getPopulationId()); 326 for (String field : inputErrors.keySet()) 327 { 328 errors.putAll(field, inputErrors.get(field).getErrors()); 329 } 330 331 if (errors.isEmpty()) 332 { 333 try 334 { 335 // Validation passed: effectively change the password. 336 _userSignupManager.changeUserPassword(siteName, foUser.getLogin(), token, password, foUser.getPopulationId()); 337 } 338 catch (UserManagementException e) 339 { 340 StatusError statusError = e.getStatusError(); 341 switch (statusError) 342 { 343 case TOKEN_UNKNOWN: 344 case TOKEN_EXPIRED: 345 setGlobalError(statusError, errors, foUser.getLogin(), foUser.getPopulationId()); 346 break; 347 default: 348 throw e; 349 } 350 } 351 } 352 } 353 354 /** 355 * Set the global error if there is one. 356 * @param error The error to add (can be null) 357 * @param errors The errors map 358 * @param login The login of the user (can be null) 359 * @param populationId The population of the user (can be null) 360 */ 361 protected void setGlobalError(StatusError error, Multimap<String, I18nizableText> errors, String login, String populationId) 362 { 363 if (error != null) 364 { 365 errors.put("global", new I18nizableText(error.name())); 366 if (getLogger().isWarnEnabled()) 367 { 368 String message = String.format( 369 "Error during resetting the password of the connected user '%s' of population '%s': %s", 370 Optional.ofNullable(login).orElse(StringUtils.EMPTY), 371 Optional.ofNullable(populationId).orElse(StringUtils.EMPTY), 372 error.name()); 373 getLogger().warn(message); 374 } 375 } 376 } 377}