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}