001/*
002 *  Copyright 2015 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.plugins.core.user;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021
022import org.apache.avalon.framework.component.Component;
023import org.apache.avalon.framework.context.Context;
024import org.apache.avalon.framework.context.ContextException;
025import org.apache.avalon.framework.context.Contextualizable;
026import org.apache.avalon.framework.logger.AbstractLogEnabled;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.cocoon.ProcessingException;
031import org.apache.cocoon.components.ContextHelper;
032import org.apache.cocoon.environment.Redirector;
033import org.apache.cocoon.environment.Request;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.core.authentication.AbstractCredentialProvider;
037import org.ametys.core.authentication.AuthenticateAction;
038import org.ametys.core.authentication.BlockingCredentialProvider;
039import org.ametys.core.right.RightManager;
040import org.ametys.core.ui.Callable;
041import org.ametys.core.user.CurrentUserProvider;
042import org.ametys.core.user.InvalidModificationException;
043import org.ametys.core.user.User;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.core.user.UserManager;
046import org.ametys.core.user.directory.ModifiableUserDirectory;
047import org.ametys.core.user.directory.UserDirectory;
048import org.ametys.core.user.population.UserPopulation;
049import org.ametys.core.user.population.UserPopulationDAO;
050import org.ametys.runtime.authentication.AccessDeniedException;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.runtime.model.DefinitionContext;
053import org.ametys.runtime.model.View;
054import org.ametys.runtime.parameter.Errors;
055
056/**
057 * DAO for manipulating {@link User}
058 *
059 */
060public class UserDAO extends AbstractLogEnabled implements Component, Contextualizable, Serviceable 
061{
062    /** The avalon role */
063    public static final String ROLE = UserDAO.class.getName();
064    
065    /** The service manager */
066    protected ServiceManager _smanager;
067    /** The user manager */
068    protected UserManager _userManager;
069    /** The user population DAO */
070    protected UserPopulationDAO _userPopulationDAO;
071    /** The current user provider. */
072    protected CurrentUserProvider _currentUserProvider;
073    /** The Avalon context */
074    protected Context _context;
075    /** The user helper */
076    protected UserHelper _userHelper;
077    /** The right manager */
078    protected RightManager _rightManager;
079
080    public void contextualize(Context context) throws ContextException
081    {
082        _context = context;
083    }
084    
085    public void service(ServiceManager smanager) throws ServiceException
086    {
087        _smanager = smanager;
088        _userManager = (UserManager) smanager.lookup(UserManager.ROLE);
089        _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE);
090        _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE);
091        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
092    }
093    
094    /**
095     * Get user's information
096     * @param login The user's login
097     * @param populationId The id of the population
098     * @return The user's information
099     */
100    @Callable
101    public Map<String, Object> getUser (String login, String populationId)
102    {
103        return _userHelper.user2json(_userManager.getUser(populationId, login), true);
104    }
105    
106    /**
107     * Checks if the user is modifiable
108     * @param login The users's login
109     * @param populationId The id of the population of the user
110     * @return A map with the "isModifiable" at true if the user is modifiable
111     */
112    @Callable
113    public Map<String, Object> isModifiable (String login, String populationId)
114    {
115        Map<String, Object> result = new HashMap<>();
116        result.put("isModifiable", _userManager.getUserDirectory(populationId, login) instanceof ModifiableUserDirectory);
117        result.put("additionalDescription", new I18nizableText("plugin.core-ui", "PLUGINS_CORE_UI_USERS_EDIT_NO_MODIFIABLE_DESCRIPTION"));
118        return result;
119    }
120    
121    /**
122     * Checks if the user is removable
123     * @param login The users's login
124     * @param populationId The id of the population of the user
125     * @return A map with the "isRemovable" at true if the user is removable
126     */
127    @Callable
128    public Map<String, Object> isRemovable (String login, String populationId)
129    {
130        Map<String, Object> result = new HashMap<>();
131        
132        UserDirectory userDirectory = _userManager.getUserDirectory(populationId, login);
133        if (userDirectory != null && populationId.equals(UserPopulationDAO.ADMIN_POPULATION_ID) && userDirectory.getUsers().size() == 1)
134        {
135            // Impossible to delete the last user of the admin population
136            result.put("isRemovable", false);
137            result.put("additionalDescription", new I18nizableText("plugin.core-ui", "PLUGINS_CORE_UI_USERS_DELETE_LAST_ADMIN_DESCRIPTION"));
138        }
139        else
140        {
141            result.put("isRemovable", userDirectory instanceof ModifiableUserDirectory);
142            result.put("additionalDescription", new I18nizableText("plugin.core-ui", "PLUGINS_CORE_UI_USERS_DELETE_NO_MODIFIABLE_DESCRIPTION"));
143        }
144        return result;
145    }
146    
147    /**
148     * Creates a User
149     * @param populationId The id of the user population
150     * @param userDirectoryId The id of the user directory
151     * @param untypedValues The untyped user's parameters
152     * @return The created user as JSON object
153     * @throws InvalidModificationException If modification is not possible 
154     */
155    @Callable (right = "Runtime_Rights_User_Handle")
156    public Map<String, Object> addUser (String populationId, String userDirectoryId, Map<String, String> untypedValues) throws InvalidModificationException
157    {
158        Map<String, Object> result = new HashMap<>();
159        
160        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId);
161        UserDirectory userDirectory = userPopulation.getUserDirectory(userDirectoryId);
162        
163        if (!(userDirectory instanceof ModifiableUserDirectory))
164        {
165            getLogger().error("Users are not modifiable !");
166            throw new InvalidModificationException("Users are not modifiable !");
167        }
168        
169        ModifiableUserDirectory modifiableUserDirectory = (ModifiableUserDirectory) userDirectory;
170        
171        String login = untypedValues.get("login");
172        
173        try
174        {
175            if (getLogger().isInfoEnabled())
176            {
177                getLogger().info(String.format("User %s is adding a new user '%s'", _getCurrentUser(), login));
178            }
179
180            modifiableUserDirectory.add(untypedValues);
181            return _userHelper.user2json(modifiableUserDirectory.getUser(login), true);
182        }
183        catch (InvalidModificationException e)
184        {
185            Map<String, Errors> fieldErrors = e.getFieldErrors();
186            
187            if (fieldErrors != null && fieldErrors.size() > 0)
188            {
189                result.put("errors", fieldErrors);
190            }
191            else
192            {
193                throw e;
194            }
195        }
196
197        if (getLogger().isDebugEnabled())
198        {
199            getLogger().debug("Ending user's edition");
200        }
201        
202        return result;
203    }
204    
205    /**
206     * Edits a User
207     * @param populationId The id of the population of the user to edit
208     * @param untypedValues The untyped user's parameters
209     * @return The update user as JSON object
210     * @throws InvalidModificationException If modification is not possible 
211     */
212    @Callable (right = "Runtime_Rights_User_Handle")
213    public Map<String, Object> editUser (String populationId, Map<String, String> untypedValues) throws InvalidModificationException
214    {
215        Map<String, Object> result = new HashMap<>();
216        
217        String login = untypedValues.get("login");
218        UserDirectory userDirectory = _userManager.getUserDirectory(populationId, login);
219        
220        if (!(userDirectory instanceof ModifiableUserDirectory))
221        {
222            getLogger().error("Users are not modifiable !");
223            throw new InvalidModificationException("Users are not modifiable !");
224        }
225        
226        ModifiableUserDirectory modifiableUserDirectory = (ModifiableUserDirectory) userDirectory;
227        
228        try
229        {
230            if (getLogger().isInfoEnabled())
231            {
232                getLogger().info(String.format("User %s is updating information about user '%s'", _getCurrentUser(), login));
233            }
234
235            modifiableUserDirectory.update(untypedValues);
236            return _userHelper.user2json(modifiableUserDirectory.getUser(login), true);
237        }
238        catch (InvalidModificationException e)
239        {
240            Map<String, Errors> fieldErrors = e.getFieldErrors();
241            
242            if (fieldErrors != null && fieldErrors.size() > 0)
243            {
244                result.put("errors", fieldErrors);
245            }
246            else
247            {
248                throw e;
249            }
250        }
251
252        if (getLogger().isDebugEnabled())
253        {
254            getLogger().debug("Ending user's edition");
255        }
256        
257        return result;
258    }
259    
260    /**
261     * Deletes users
262     * @param users The users to delete
263     * @throws InvalidModificationException If modification is not possible 
264     */
265    @Callable (right = "Runtime_Rights_User_Handle")
266    public void deleteUsers (List<Map<String, String>> users) throws InvalidModificationException
267    {
268        for (Map<String, String> user : users)
269        {
270            String login = user.get("login");
271            String populationId = user.get("populationId");
272            _deleteUser(login, populationId);
273        }
274       
275        if (getLogger().isDebugEnabled())
276        {
277            getLogger().debug("Ending user's removal");
278        }
279    }
280    
281    private void _deleteUser(String login, String populationId) throws InvalidModificationException
282    {
283        UserDirectory userDirectory = _userManager.getUserDirectory(populationId, login);
284        
285        if (!(userDirectory instanceof ModifiableUserDirectory))
286        {
287            getLogger().error("Users are not modifiable !");
288            throw new InvalidModificationException("Users are not modifiable !");
289        }
290        
291        if (populationId.equals(UserPopulationDAO.ADMIN_POPULATION_ID) && userDirectory.getUsers().size() == 1)
292        {
293            // Impossible to delete the last user of the admin population
294            getLogger().error("Deletion forbidden: last user of the 'admin' population.");
295            throw new InvalidModificationException("You cannot delete the last user of the 'admin' population !");
296        }
297        
298        ModifiableUserDirectory modifiableUserDirectory = (ModifiableUserDirectory) userDirectory;
299        
300        if (getLogger().isInfoEnabled())
301        {
302            getLogger().info(String.format("User %s is removing user '%s'", _getCurrentUser(), login));
303        }
304        
305        modifiableUserDirectory.remove(login);
306    }
307    
308    /**
309     * Get the users edition model 
310     * @param login The user's login to edit
311     * @param populationId The id of the population of the user to edit
312     * @throws InvalidModificationException If modification is not possible 
313     * @throws ProcessingException If there is another exception
314     * @return The edition model as an object
315     */
316    @Callable (right = "Runtime_Rights_User_Handle")
317    public Map<String, Object> getEditionModelForUSer(String login, String populationId) throws InvalidModificationException, ProcessingException
318    {
319        UserDirectory userDirectory = _userManager.getUserDirectory(populationId, login);
320        return _getEditionModel(userDirectory);
321    }
322    
323    /**
324     * Get the users edition model 
325     * @param populationId The id of the population where to add a user
326     * @param userDirectoryId The id of the user directory where to add a user
327     * @throws InvalidModificationException If modification is not possible 
328     * @throws ProcessingException If there is another exception
329     * @return The edition model as an object
330     */
331    @Callable (right = "Runtime_Rights_User_Handle")
332    public Map<String, Object> getEditionModelForDirectory(String populationId, String userDirectoryId) throws InvalidModificationException, ProcessingException
333    {
334        UserDirectory userDirectory = _userPopulationDAO.getUserPopulation(populationId).getUserDirectory(userDirectoryId);
335        return _getEditionModel(userDirectory);
336    }
337    
338    private Map<String, Object> _getEditionModel (UserDirectory userDirectory) throws InvalidModificationException, ProcessingException
339    {
340        if (!(userDirectory instanceof ModifiableUserDirectory))
341        {
342            getLogger().error("Users are not modifiable !");
343            throw new InvalidModificationException("Users are not modifiable !");
344        }
345        
346        ModifiableUserDirectory modifiableUserDirectory = (ModifiableUserDirectory) userDirectory;
347        View view = modifiableUserDirectory.getView();
348        return view.toJSON(DefinitionContext.newInstance().withEdition(true));
349    }
350    
351    /**
352     * Impersonate the selected user
353     * @param login the login of the user to impersonate
354     * @param populationId The id of the population
355     * @return a map of information on the user
356     * @throws AccessDeniedException If the currently connected user has no right to do so
357     */
358    @Callable (right = "Runtime_Rights_User_Handle", context = "/admin")
359    public Map<String, String> impersonate(String login, String populationId) throws AccessDeniedException
360    {
361        UserIdentity currentUser = _getCurrentUser();
362        
363        if (StringUtils.isEmpty(login))
364        {
365            throw new IllegalArgumentException("'login' parameter is null or empty");
366        }
367        
368        Map<String, String> result = new HashMap<>();
369        
370        User user = _userManager.getUser(populationId, login);
371        if (user == null)
372        {
373            result.put("error", "unknown-user");   
374        }
375        else
376        {
377            try
378            {
379                _currentUserProvider.logout();
380            }
381            catch (ProcessingException e)
382            {
383                getLogger().error("An error occurred while logging out current user " + currentUser);
384            }
385            
386            Request request = ContextHelper.getRequest(_context);
387            
388            AuthenticateAction.setUserIdentityInSession(request, user.getIdentity(), new ImpersonateCredentialProvider(), true);
389            
390            result.put("login", login);
391            result.put("populationId", populationId);
392            result.put("name", user.getFullName());
393            
394            if (getLogger().isInfoEnabled())
395            {
396                getLogger().info("Impersonation of the user '" + login + "' from IP " + request.getRemoteAddr() + " done by " + currentUser);
397            }
398        }
399        
400        return result;
401    }
402    
403    /**
404     * Provides the login of the current user.
405     * @return the login which cannot be <code>null</code>.
406     */
407    protected UserIdentity _getCurrentUser()
408    {
409        if (_currentUserProvider == null)
410        {
411            try
412            {
413                _currentUserProvider = (CurrentUserProvider) _smanager.lookup(CurrentUserProvider.ROLE);
414            }
415            catch (ServiceException e)
416            {
417                throw new IllegalStateException(e);
418            }
419        }
420        
421        return _currentUserProvider.getUser();
422    }
423    
424    /**
425     * A fake credential provider used by the impersonate process
426     */
427    public static class ImpersonateCredentialProvider extends AbstractCredentialProvider implements BlockingCredentialProvider
428    {
429
430        public boolean blockingGrantAnonymousRequest()
431        {
432            return false;
433        }
434
435        public boolean blockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception
436        {
437            return true;
438        }
439
440        public UserIdentity blockingGetUserIdentity(Redirector redirector) throws Exception
441        {
442            return null;
443        }
444
445        public void blockingUserNotAllowed(Redirector redirector) throws Exception
446        {
447            // nothing
448        }
449
450        public void blockingUserAllowed(UserIdentity userIdentity)
451        {
452            // nothing
453        }
454
455        public boolean requiresNewWindow()
456        {
457            return false;
458        }
459        
460    }
461}