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