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