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