001/*
002 * (C) Copyright 2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 * Contributors:
014 *     Nuxeo - initial API and implementation
015 */
016
017package org.nuxeo.ecm.webapp.security;
018
019import static org.jboss.seam.ScopeType.APPLICATION;
020import static org.jboss.seam.ScopeType.CONVERSATION;
021import static org.jboss.seam.annotations.Install.FRAMEWORK;
022import static org.nuxeo.ecm.platform.ui.web.api.WebActions.CURRENT_TAB_CHANGED_EVENT;
023import static org.nuxeo.ecm.platform.ui.web.api.WebActions.CURRENT_TAB_SELECTED_EVENT;
024import static org.nuxeo.ecm.user.invite.UserInvitationService.ValidationMethod.EMAIL;
025
026import java.io.Serializable;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import javax.faces.application.FacesMessage;
032import javax.faces.component.UIComponent;
033import javax.faces.component.UIInput;
034import javax.faces.context.FacesContext;
035import javax.faces.validator.ValidatorException;
036
037import org.apache.commons.lang.StringUtils;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.jboss.seam.annotations.Factory;
041import org.jboss.seam.annotations.Install;
042import org.jboss.seam.annotations.Name;
043import org.jboss.seam.annotations.Observer;
044import org.jboss.seam.annotations.Scope;
045import org.jboss.seam.core.Events;
046import org.jboss.seam.international.StatusMessage;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.NuxeoPrincipal;
049import org.nuxeo.ecm.core.api.repository.RepositoryManager;
050import org.nuxeo.ecm.directory.BaseSession;
051import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
052import org.nuxeo.ecm.platform.usermanager.NuxeoPrincipalImpl;
053import org.nuxeo.ecm.platform.usermanager.UserAdapter;
054import org.nuxeo.ecm.platform.usermanager.UserAdapterImpl;
055import org.nuxeo.ecm.platform.usermanager.exceptions.UserAlreadyExistsException;
056import org.nuxeo.ecm.user.invite.UserInvitationService;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * Handles users management related web actions.
061 *
062 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
063 * @since 5.4.2
064 */
065@Name("userManagementActions")
066@Scope(CONVERSATION)
067@Install(precedence = FRAMEWORK)
068public class UserManagementActions extends AbstractUserGroupManagement implements Serializable {
069
070    private static final long serialVersionUID = 1L;
071
072    private static final Log log = LogFactory.getLog(UserManagementActions.class);
073
074    public static final String USERS_TAB = USER_CENTER_CATEGORY + ":" + USERS_GROUPS_HOME + ":" + "UsersHome";
075
076    public static final String USERS_LISTING_CHANGED = "usersListingChanged";
077
078    public static final String USERS_SEARCH_CHANGED = "usersSearchChanged";
079
080    public static final String USER_SELECTED_CHANGED = "selectedUserChanged";
081
082    public static final String SELECTED_LETTER_CHANGED = "selectedLetterChanged";
083
084    protected String selectedLetter = "";
085
086    protected DocumentModel selectedUser;
087
088    protected DocumentModel newUser;
089
090    protected boolean immediateCreation = false;
091
092    protected boolean createAnotherUser = false;
093
094    protected String defaultRepositoryName = null;
095
096    @Override
097    protected String computeListingMode() {
098        return userManager.getUserListingMode();
099    }
100
101    public DocumentModel getSelectedUser() {
102        shouldResetStateOnTabChange = true;
103        return selectedUser;
104    }
105
106    public void setSelectedUser(DocumentModel user) {
107        fireSeamEvent(USER_SELECTED_CHANGED);
108        selectedUser = user;
109    }
110
111    /**
112     * @deprecated since version 5.5, use {@link #setSelectedUserName} instead.
113     */
114    @Deprecated
115    public void setSelectedUser(String userName) {
116        setSelectedUser(refreshUser(userName));
117    }
118
119    /**
120     * UserRegistrationService userRegistrationService = Framework.getLocalService(UserRegistrationService.class);
121     *
122     * @since 5.5
123     */
124    public void setSelectedUserName(String userName) {
125        setSelectedUser(refreshUser(userName));
126    }
127
128    public String getSelectedUserName() {
129        return selectedUser.getId();
130    }
131
132    // refresh to get references
133    protected DocumentModel refreshUser(String userName) {
134        return userManager.getUserModel(userName);
135    }
136
137    public String getSelectedLetter() {
138        return selectedLetter;
139    }
140
141    public void setSelectedLetter(String selectedLetter) {
142        if (selectedLetter != null && !selectedLetter.equals(this.selectedLetter)) {
143            this.selectedLetter = selectedLetter;
144            fireSeamEvent(SELECTED_LETTER_CHANGED);
145        }
146        this.selectedLetter = selectedLetter;
147    }
148
149    public DocumentModel getNewUser() {
150        if (newUser == null) {
151            newUser = userManager.getBareUserModel();
152        }
153        return newUser;
154    }
155
156    public boolean getAllowEditUser() {
157        return selectedUser != null && getCanEditUsers(true) && !BaseSession.isReadOnlyEntry(selectedUser);
158    }
159
160    protected boolean getCanEditUsers(boolean allowCurrentUser) {
161        if (userManager.areUsersReadOnly()) {
162            return false;
163        }
164
165        // if the selected user is the anonymous user, do not display
166        // edit/password tabs
167        if (selectedUser != null && userManager.getAnonymousUserId() != null
168                && userManager.getAnonymousUserId().equals(selectedUser.getId())) {
169
170            return false;
171        }
172
173        if (selectedUser != null) {
174            NuxeoPrincipal selectedPrincipal = userManager.getPrincipal(selectedUser.getId());
175            if (selectedPrincipal.isAdministrator() && !((NuxeoPrincipal) currentUser).isAdministrator()) {
176                return false;
177            }
178        }
179
180        if (currentUser instanceof NuxeoPrincipal) {
181            NuxeoPrincipal pal = (NuxeoPrincipal) currentUser;
182            if (webActions.checkFilter(USERS_GROUPS_MANAGEMENT_ACCESS_FILTER)) {
183                return true;
184            }
185            if (allowCurrentUser && selectedUser != null) {
186                if (pal.getName().equals(selectedUser.getId())) {
187                    return true;
188                }
189            }
190        }
191        return false;
192    }
193
194    public boolean getAllowChangePassword() {
195        return selectedUser != null && getCanEditUsers(true) && !BaseSession.isReadOnlyEntry(selectedUser);
196    }
197
198    public boolean getAllowCreateUser() {
199        return getCanEditUsers(false);
200    }
201
202    public boolean getAllowDeleteUser() {
203        return selectedUser != null && getCanEditUsers(false) && !BaseSession.isReadOnlyEntry(selectedUser);
204    }
205
206    public void clearSearch() {
207        searchString = null;
208        fireSeamEvent(USERS_SEARCH_CHANGED);
209    }
210
211    public void createUser() {
212        try {
213            if (immediateCreation) {
214                // Create the user with password
215                setSelectedUser(userManager.createUser(newUser));
216                // Set the default value for the creation
217                immediateCreation = false;
218                facesMessages.add(StatusMessage.Severity.INFO,
219                        resourcesAccessor.getMessages().get("info.userManager.userCreated"));
220                if (createAnotherUser) {
221                    showCreateForm = true;
222                } else {
223                    showCreateForm = false;
224                    showUserOrGroup = true;
225                    detailsMode = null;
226                }
227                fireSeamEvent(USERS_LISTING_CHANGED);
228            } else {
229                UserInvitationService userRegistrationService = Framework.getLocalService(UserInvitationService.class);
230                Map<String, Serializable> additionalInfos = new HashMap<String, Serializable>();
231                // Wrap the form as an invitation to the user
232                UserAdapter newUserAdapter = new UserAdapterImpl(newUser, userManager);
233                DocumentModel userRegistrationDoc = wrapToUserRegistration(newUserAdapter);
234                userRegistrationService.submitRegistrationRequest(userRegistrationDoc, additionalInfos, EMAIL, true);
235
236                facesMessages.add(StatusMessage.Severity.INFO,
237                        resourcesAccessor.getMessages().get("info.userManager.userInvited"));
238                if (createAnotherUser) {
239                    showCreateForm = true;
240                } else {
241                    showCreateForm = false;
242                    showUserOrGroup = false;
243                    detailsMode = null;
244                }
245
246            }
247            newUser = null;
248
249        } catch (UserAlreadyExistsException e) {
250            facesMessages.add(StatusMessage.Severity.ERROR,
251                    resourcesAccessor.getMessages().get("error.userManager.userAlreadyExists"));
252        } catch (Exception e) {
253            String message = e.getLocalizedMessage();
254            if (e.getCause() != null) {
255                message += e.getCause().getLocalizedMessage();
256            }
257            log.error(message, e);
258
259            facesMessages.add(StatusMessage.Severity.ERROR, message);
260
261        }
262    }
263
264    private String getDefaultRepositoryName() {
265        if (defaultRepositoryName == null) {
266            try {
267                defaultRepositoryName = Framework.getService(RepositoryManager.class).getDefaultRepository().getName();
268            } catch (Exception e) {
269                throw new RuntimeException(e);
270            }
271        }
272        return defaultRepositoryName;
273    }
274
275    public void updateUser() {
276        UpdateUserUnrestricted runner = new UpdateUserUnrestricted(getDefaultRepositoryName(), selectedUser);
277        runner.runUnrestricted();
278
279        detailsMode = DETAILS_VIEW_MODE;
280        fireSeamEvent(USERS_LISTING_CHANGED);
281    }
282
283    public String changePassword() {
284        updateUser();
285        detailsMode = DETAILS_VIEW_MODE;
286
287        String message = resourcesAccessor.getMessages().get("label.userManager.password.changed");
288        facesMessages.add(FacesMessage.SEVERITY_INFO, message);
289        fireSeamEvent(USERS_LISTING_CHANGED);
290
291        return null;
292    }
293
294    public void deleteUser() {
295        userManager.deleteUser(selectedUser);
296        selectedUser = null;
297        showUserOrGroup = false;
298        fireSeamEvent(USERS_LISTING_CHANGED);
299    }
300
301    public void validateUserName(FacesContext context, UIComponent component, Object value) {
302        if (!(value instanceof String) || !StringUtils.containsOnly((String) value, VALID_CHARS)) {
303            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
304                    ComponentUtils.translate(context, "label.userManager.wrong.username"), null);
305            // also add global message
306            context.addMessage(null, message);
307            throw new ValidatorException(message);
308        }
309    }
310
311    /**
312     * Verify that only administrators can add administrator groups.
313     *
314     * @param context
315     * @param component
316     * @param value
317     * @since 5.9.2
318     */
319    public void validateGroups(FacesContext context, UIComponent component, Object value) {
320
321        UIInput groupsComponent = getReferencedComponent("groupsValueHolderId", component);
322
323        @SuppressWarnings("unchecked")
324        List<String> groups = groupsComponent == null ? null : (List<String>) groupsComponent.getLocalValue();
325        if (groups == null || groups.isEmpty()) {
326            return;
327        }
328        if (!isAllowedToAdminGroups(groups)) {
329            throwValidationException(context, "label.userManager.invalidGroupSelected");
330        }
331    }
332
333    /**
334     * Checks if the current user is allowed to aministrate (meaning add/remove) the given groups.
335     *
336     * @param groups
337     * @return
338     * @since 5.9.2
339     */
340    boolean isAllowedToAdminGroups(List<String> groups) {
341        NuxeoPrincipalImpl nuxeoPrincipal = (NuxeoPrincipalImpl) currentUser;
342
343        if (!nuxeoPrincipal.isAdministrator()) {
344            List<String> adminGroups = getAllAdminGroups();
345
346            for (String group : groups) {
347                if (adminGroups.contains(group)) {
348                    return false;
349                }
350            }
351
352        }
353        return true;
354    }
355
356    /**
357     * Throw a validation exception with a translated message that is show in the UI.
358     *
359     * @param context the current faces context
360     * @param message the error message
361     * @param messageArgs the parameters for the message
362     * @since 5.9.2
363     */
364    private void throwValidationException(FacesContext context, String message, Object... messageArgs) {
365        FacesMessage fmessage = new FacesMessage(FacesMessage.SEVERITY_ERROR,
366                ComponentUtils.translate(context, message, messageArgs), null);
367        throw new ValidatorException(fmessage);
368    }
369
370    /**
371     * Return the value of the JSF component who's id is references in an attribute of the componet passed in parameter.
372     *
373     * @param string the attribute holding the target component id
374     * @param component the component holding the attribute
375     * @return the UIInput component, null otherwise
376     * @since 5.9.2
377     */
378    private UIInput getReferencedComponent(String attribute, UIComponent component) {
379        Map<String, Object> attributes = component.getAttributes();
380        String targetComponentId = (String) attributes.get(attribute);
381
382        if (targetComponentId == null) {
383            log.error(String.format("Target component id (%s) not found in attributes", attribute));
384            return null;
385        }
386
387        UIInput targetComponent = (UIInput) component.findComponent(targetComponentId);
388        if (targetComponent == null) {
389            return null;
390        }
391
392        return targetComponent;
393    }
394
395    public void validatePassword(FacesContext context, UIComponent component, Object value) {
396
397        Object firstPassword = getReferencedComponent("firstPasswordInputId", component).getLocalValue();
398        Object secondPassword = getReferencedComponent("secondPasswordInputId", component).getLocalValue();
399
400        if (firstPassword == null || secondPassword == null) {
401            log.error("Cannot validate passwords: value(s) not found");
402            return;
403        }
404
405        if (!firstPassword.equals(secondPassword)) {
406            throwValidationException(context, "label.userManager.password.not.match");
407        }
408
409    }
410
411    private DocumentModel wrapToUserRegistration(UserAdapter newUserAdapter) {
412        UserInvitationService userRegistrationService = Framework.getLocalService(UserInvitationService.class);
413        DocumentModel newUserRegistration = userRegistrationService.getUserRegistrationModel(null);
414
415        // Map the values from the object filled in the form
416        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoUsernameField(),
417                newUserAdapter.getName());
418        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoFirstnameField(),
419                newUserAdapter.getFirstName());
420        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoLastnameField(),
421                newUserAdapter.getLastName());
422        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoEmailField(),
423                newUserAdapter.getEmail());
424        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoGroupsField(),
425                newUserAdapter.getGroups().toArray());
426        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoCompanyField(),
427                newUserAdapter.getCompany());
428
429        return newUserRegistration;
430    }
431
432    @Factory(value = "notReadOnly", scope = APPLICATION)
433    public boolean isNotReadOnly() {
434        return !Framework.isBooleanPropertyTrue("org.nuxeo.ecm.webapp.readonly.mode");
435    }
436
437    public List<String> getUserVirtualGroups(String userId) {
438        NuxeoPrincipal principal = userManager.getPrincipal(userId);
439        if (principal instanceof NuxeoPrincipalImpl) {
440            NuxeoPrincipalImpl user = (NuxeoPrincipalImpl) principal;
441            return user.getVirtualGroups();
442        }
443        return null;
444    }
445
446    public String viewUser(String userName) {
447        webActions.setCurrentTabIds(MAIN_TAB_HOME + "," + USERS_TAB);
448        setSelectedUser(userName);
449        setShowUser(Boolean.TRUE.toString());
450        return VIEW_HOME;
451    }
452
453    public String viewUser() {
454        if (selectedUser != null) {
455            return viewUser(selectedUser.getId());
456        } else {
457            return null;
458        }
459    }
460
461    /**
462     * @since 5.5
463     */
464    public void setShowUser(String showUser) {
465        showUserOrGroup = Boolean.valueOf(showUser);
466        // do not reset the state before actually viewing the user
467        shouldResetStateOnTabChange = false;
468    }
469
470    protected void fireSeamEvent(String eventName) {
471        Events evtManager = Events.instance();
472        evtManager.raiseEvent(eventName);
473    }
474
475    @Factory(value = "anonymousUserDefined", scope = APPLICATION)
476    public boolean anonymousUserDefined() {
477        return userManager.getAnonymousUserId() != null;
478    }
479
480    @Observer(value = { USERS_LISTING_CHANGED })
481    public void onUsersListingChanged() {
482        contentViewActions.refreshOnSeamEvent(USERS_LISTING_CHANGED);
483        contentViewActions.resetPageProviderOnSeamEvent(USERS_LISTING_CHANGED);
484    }
485
486    @Observer(value = { USERS_SEARCH_CHANGED })
487    public void onUsersSearchChanged() {
488        contentViewActions.refreshOnSeamEvent(USERS_SEARCH_CHANGED);
489        contentViewActions.resetPageProviderOnSeamEvent(USERS_SEARCH_CHANGED);
490    }
491
492    @Observer(value = { SELECTED_LETTER_CHANGED })
493    public void onSelectedLetterChanged() {
494        contentViewActions.refreshOnSeamEvent(SELECTED_LETTER_CHANGED);
495        contentViewActions.resetPageProviderOnSeamEvent(SELECTED_LETTER_CHANGED);
496    }
497
498    @Observer(value = { CURRENT_TAB_CHANGED_EVENT + "_" + MAIN_TABS_CATEGORY,
499            CURRENT_TAB_CHANGED_EVENT + "_" + NUXEO_ADMIN_CATEGORY,
500            CURRENT_TAB_CHANGED_EVENT + "_" + USER_CENTER_CATEGORY,
501            CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB,
502            CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB,
503            CURRENT_TAB_SELECTED_EVENT + "_" + MAIN_TABS_CATEGORY,
504            CURRENT_TAB_SELECTED_EVENT + "_" + NUXEO_ADMIN_CATEGORY,
505            CURRENT_TAB_SELECTED_EVENT + "_" + USER_CENTER_CATEGORY,
506            CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB,
507            CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB })
508    public void resetState() {
509        if (shouldResetStateOnTabChange) {
510            newUser = null;
511            selectedUser = null;
512            showUserOrGroup = false;
513            showCreateForm = false;
514            immediateCreation = false;
515            detailsMode = DETAILS_VIEW_MODE;
516        }
517    }
518
519    /**
520     * @return The type of creation for the user.
521     * @since 5.9.3
522     */
523    public boolean isImmediateCreation() {
524        return immediateCreation;
525    }
526
527    /**
528     * @param immediateCreation
529     * @since 5.9.3
530     */
531    public void setImmediateCreation(boolean immediateCreation) {
532        this.immediateCreation = immediateCreation;
533    }
534
535    public boolean isCreateAnotherUser() {
536        return createAnotherUser;
537    }
538
539    public void setCreateAnotherUser(boolean createAnotherUser) {
540        this.createAnotherUser = createAnotherUser;
541    }
542
543}