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() && !((NuxeoPrincipal) currentUser).isAdministrator()) {
182                return false;
183            }
184        }
185
186        if (currentUser instanceof NuxeoPrincipal) {
187            NuxeoPrincipal pal = (NuxeoPrincipal) currentUser;
188            if (webActions.checkFilter(USERS_GROUPS_MANAGEMENT_ACCESS_FILTER)) {
189                return true;
190            }
191            if (allowCurrentUser && selectedUser != null) {
192                if (pal.getName().equals(selectedUser.getId())) {
193                    return true;
194                }
195            }
196        }
197        return false;
198    }
199
200    public boolean getAllowChangePassword() {
201        return selectedUser != null && getCanEditUsers(true) && !BaseSession.isReadOnlyEntry(selectedUser);
202    }
203
204    public boolean getAllowCreateUser() {
205        return getCanEditUsers(false);
206    }
207
208    public boolean getAllowDeleteUser() {
209        return selectedUser != null && getCanEditUsers(false) && !BaseSession.isReadOnlyEntry(selectedUser);
210    }
211
212    public void clearSearch() {
213        searchString = null;
214        fireSeamEvent(USERS_SEARCH_CHANGED);
215    }
216
217    public void createUser() {
218        try {
219            if (immediateCreation) {
220                // Create the user with password
221                setSelectedUser(userManager.createUser(newUser));
222                // Set the default value for the creation
223                immediateCreation = false;
224                facesMessages.add(StatusMessage.Severity.INFO,
225                        resourcesAccessor.getMessages().get("info.userManager.userCreated"));
226                if (createAnotherUser) {
227                    showCreateForm = true;
228                } else {
229                    showCreateForm = false;
230                    showUserOrGroup = true;
231                    detailsMode = null;
232                }
233                fireSeamEvent(USERS_LISTING_CHANGED);
234            } else {
235                UserInvitationService userRegistrationService = Framework.getService(UserInvitationService.class);
236                Map<String, Serializable> additionalInfos = new HashMap<String, Serializable>();
237                additionalInfos.put(UserInvitationComponent.PARAM_ORIGINATING_USER , currentUser.getName());
238                // Wrap the form as an invitation to the user
239                UserAdapter newUserAdapter = new UserAdapterImpl(newUser, userManager);
240                DocumentModel userRegistrationDoc = wrapToUserRegistration(newUserAdapter);
241                userRegistrationService.submitRegistrationRequest(userRegistrationDoc, additionalInfos, EMAIL, true);
242
243                facesMessages.add(StatusMessage.Severity.INFO,
244                        resourcesAccessor.getMessages().get("info.userManager.userInvited"));
245                if (createAnotherUser) {
246                    showCreateForm = true;
247                } else {
248                    showCreateForm = false;
249                    showUserOrGroup = false;
250                    detailsMode = null;
251                }
252
253            }
254            newUser = null;
255
256        } catch (UserAlreadyExistsException e) {
257            facesMessages.add(StatusMessage.Severity.ERROR,
258                    resourcesAccessor.getMessages().get("error.userManager.userAlreadyExists"));
259        } catch (InvalidPasswordException e) {
260            facesMessages.add(StatusMessage.Severity.ERROR,
261                    resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
262        } catch (Exception e) {
263            String message = e.getLocalizedMessage();
264            if (e.getCause() != null) {
265                message += e.getCause().getLocalizedMessage();
266            }
267            log.error(message, e);
268
269            facesMessages.add(StatusMessage.Severity.ERROR, message);
270
271        }
272    }
273
274    private String getDefaultRepositoryName() {
275        if (defaultRepositoryName == null) {
276            try {
277                defaultRepositoryName = Framework.getService(RepositoryManager.class).getDefaultRepository().getName();
278            } catch (Exception e) {
279                throw new RuntimeException(e);
280            }
281        }
282        return defaultRepositoryName;
283    }
284
285    public void updateUser() {
286        try {
287            UpdateUserUnrestricted runner = new UpdateUserUnrestricted(getDefaultRepositoryName(), selectedUser);
288            runner.runUnrestricted();
289        } catch (InvalidPasswordException e) {
290            facesMessages.add(StatusMessage.Severity.ERROR,
291                    resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
292        }
293
294        detailsMode = DETAILS_VIEW_MODE;
295        fireSeamEvent(USERS_LISTING_CHANGED);
296    }
297
298    public String changePassword() {
299        try {
300            updateUser();
301        } catch (InvalidPasswordException e) {
302            facesMessages.add(StatusMessage.Severity.ERROR,
303                    resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
304            return null;
305        }
306        detailsMode = DETAILS_VIEW_MODE;
307
308        String message = resourcesAccessor.getMessages().get("label.userManager.password.changed");
309        facesMessages.add(FacesMessage.SEVERITY_INFO, message);
310        fireSeamEvent(USERS_LISTING_CHANGED);
311
312        return null;
313    }
314
315    /**
316     * @since 8.2
317     */
318    public String updateProfilePassword() {
319
320        if (userManager.checkUsernamePassword(currentUser.getName(), oldPassword)) {
321
322            try {
323                Framework.doPrivileged(() -> userManager.updateUser(selectedUser));
324            } catch (InvalidPasswordException e) {
325                facesMessages.add(StatusMessage.Severity.ERROR,
326                        resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
327                return null;
328            }
329        } else {
330            String message = resourcesAccessor.getMessages().get("label.userManager.old.password.error");
331            facesMessages.add(FacesMessage.SEVERITY_ERROR, message);
332            return null;
333        }
334
335        String message = resourcesAccessor.getMessages().get("label.userManager.password.changed");
336        facesMessages.add(FacesMessage.SEVERITY_INFO, message);
337        detailsMode = DETAILS_VIEW_MODE;
338        fireSeamEvent(USERS_LISTING_CHANGED);
339
340        return null;
341    }
342
343    public void deleteUser() {
344        userManager.deleteUser(selectedUser);
345        selectedUser = null;
346        showUserOrGroup = false;
347        fireSeamEvent(USERS_LISTING_CHANGED);
348    }
349
350    public void validateUserName(FacesContext context, UIComponent component, Object value) {
351        if (!(value instanceof String) || !StringUtils.containsOnly((String) value, VALID_CHARS)) {
352            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
353                    ComponentUtils.translate(context, "label.userManager.wrong.username"), null);
354            // also add global message
355            context.addMessage(null, message);
356            throw new ValidatorException(message);
357        }
358    }
359
360    /**
361     * Verify that only administrators can add administrator groups.
362     *
363     * @param context
364     * @param component
365     * @param value
366     * @since 5.9.2
367     */
368    public void validateGroups(FacesContext context, UIComponent component, Object value) {
369
370        UIInput groupsComponent = getReferencedComponent("groupsValueHolderId", component);
371
372        @SuppressWarnings("unchecked")
373        List<String> groups = groupsComponent == null ? null : (List<String>) groupsComponent.getLocalValue();
374        if (groups == null || groups.isEmpty()) {
375            return;
376        }
377        if (!isAllowedToAdminGroups(groups)) {
378            throwValidationException(context, "label.userManager.invalidGroupSelected");
379        }
380    }
381
382    /**
383     * Checks if the current user is allowed to aministrate (meaning add/remove) the given groups.
384     *
385     * @param groups
386     * @return
387     * @since 5.9.2
388     */
389    boolean isAllowedToAdminGroups(List<String> groups) {
390        NuxeoPrincipalImpl nuxeoPrincipal = (NuxeoPrincipalImpl) currentUser;
391
392        if (!nuxeoPrincipal.isAdministrator()) {
393            List<String> adminGroups = getAllAdminGroups();
394
395            for (String group : groups) {
396                if (adminGroups.contains(group)) {
397                    return false;
398                }
399            }
400
401        }
402        return true;
403    }
404
405    /**
406     * Throw a validation exception with a translated message that is show in the UI.
407     *
408     * @param context the current faces context
409     * @param message the error message
410     * @param messageArgs the parameters for the message
411     * @since 5.9.2
412     */
413    private void throwValidationException(FacesContext context, String message, Object... messageArgs) {
414        FacesMessage fmessage = new FacesMessage(FacesMessage.SEVERITY_ERROR,
415                ComponentUtils.translate(context, message, messageArgs), null);
416        throw new ValidatorException(fmessage);
417    }
418
419    /**
420     * Return the value of the JSF component who's id is references in an attribute of the componet passed in parameter.
421     *
422     * @param attribute the attribute holding the target component id
423     * @param component the component holding the attribute
424     * @return the UIInput component, null otherwise
425     * @since 5.9.2
426     */
427    private UIInput getReferencedComponent(String attribute, UIComponent component) {
428        Map<String, Object> attributes = component.getAttributes();
429        String targetComponentId = (String) attributes.get(attribute);
430
431        if (targetComponentId == null) {
432            log.error(String.format("Target component id (%s) not found in attributes", attribute));
433            return null;
434        }
435
436        UIInput targetComponent = (UIInput) component.findComponent(targetComponentId);
437        if (targetComponent == null) {
438            return null;
439        }
440
441        return targetComponent;
442    }
443
444    public void validatePassword(FacesContext context, UIComponent component, Object value) {
445
446        Object firstPassword = getReferencedComponent("firstPasswordInputId", component).getLocalValue();
447        Object secondPassword = getReferencedComponent("secondPasswordInputId", component).getLocalValue();
448
449        if (firstPassword == null || secondPassword == null) {
450            log.error("Cannot validate passwords: value(s) not found");
451            return;
452        }
453
454        if (!firstPassword.equals(secondPassword)) {
455            throwValidationException(context, "label.userManager.password.not.match");
456        }
457
458    }
459
460    private DocumentModel wrapToUserRegistration(UserAdapter newUserAdapter) {
461        UserInvitationService userRegistrationService = Framework.getService(UserInvitationService.class);
462        DocumentModel newUserRegistration = userRegistrationService.getUserRegistrationModel(null);
463
464        // Map the values from the object filled in the form
465        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoUsernameField(),
466                newUserAdapter.getName());
467        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoFirstnameField(),
468                newUserAdapter.getFirstName());
469        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoLastnameField(),
470                newUserAdapter.getLastName());
471        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoEmailField(),
472                newUserAdapter.getEmail());
473        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoGroupsField(),
474                newUserAdapter.getGroups().toArray());
475        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoCompanyField(),
476                newUserAdapter.getCompany());
477
478        String tenantId = newUserAdapter.getTenantId();
479        if (StringUtils.isBlank(tenantId)) {
480            tenantId = ((NuxeoPrincipal) currentUser).getTenantId();
481        }
482        newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoTenantIdField(),
483                tenantId);
484
485        return newUserRegistration;
486    }
487
488    @Factory(value = "notReadOnly", scope = APPLICATION)
489    public boolean isNotReadOnly() {
490        return !Framework.isBooleanPropertyTrue("org.nuxeo.ecm.webapp.readonly.mode");
491    }
492
493    public List<String> getUserVirtualGroups(String userId) {
494        NuxeoPrincipal principal = userManager.getPrincipal(userId);
495        if (principal instanceof NuxeoPrincipalImpl) {
496            NuxeoPrincipalImpl user = (NuxeoPrincipalImpl) principal;
497            return user.getVirtualGroups();
498        }
499        return null;
500    }
501
502    public String viewUser(String userName) {
503        webActions.setCurrentTabIds(MAIN_TAB_HOME + "," + USERS_TAB);
504        setSelectedUser(userName);
505        setShowUser(Boolean.TRUE.toString());
506        return VIEW_HOME;
507    }
508
509    public String viewUser() {
510        if (selectedUser != null) {
511            return viewUser(selectedUser.getId());
512        } else {
513            return null;
514        }
515    }
516
517    /**
518     * @since 5.5
519     */
520    public void setShowUser(String showUser) {
521        showUserOrGroup = Boolean.valueOf(showUser);
522        // do not reset the state before actually viewing the user
523        shouldResetStateOnTabChange = false;
524    }
525
526    protected void fireSeamEvent(String eventName) {
527        Events evtManager = Events.instance();
528        evtManager.raiseEvent(eventName);
529    }
530
531    @Factory(value = "anonymousUserDefined", scope = APPLICATION)
532    public boolean anonymousUserDefined() {
533        return userManager.getAnonymousUserId() != null;
534    }
535
536    @Observer(value = { USERS_LISTING_CHANGED })
537    public void onUsersListingChanged() {
538        contentViewActions.refreshOnSeamEvent(USERS_LISTING_CHANGED);
539        contentViewActions.resetPageProviderOnSeamEvent(USERS_LISTING_CHANGED);
540    }
541
542    @Observer(value = { USERS_SEARCH_CHANGED })
543    public void onUsersSearchChanged() {
544        contentViewActions.refreshOnSeamEvent(USERS_SEARCH_CHANGED);
545        contentViewActions.resetPageProviderOnSeamEvent(USERS_SEARCH_CHANGED);
546    }
547
548    @Observer(value = { SELECTED_LETTER_CHANGED })
549    public void onSelectedLetterChanged() {
550        contentViewActions.refreshOnSeamEvent(SELECTED_LETTER_CHANGED);
551        contentViewActions.resetPageProviderOnSeamEvent(SELECTED_LETTER_CHANGED);
552    }
553
554    @Observer(value = { CURRENT_TAB_CHANGED_EVENT + "_" + MAIN_TABS_CATEGORY,
555            CURRENT_TAB_CHANGED_EVENT + "_" + NUXEO_ADMIN_CATEGORY,
556            CURRENT_TAB_CHANGED_EVENT + "_" + USER_CENTER_CATEGORY,
557            CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB,
558            CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB,
559            CURRENT_TAB_SELECTED_EVENT + "_" + MAIN_TABS_CATEGORY,
560            CURRENT_TAB_SELECTED_EVENT + "_" + NUXEO_ADMIN_CATEGORY,
561            CURRENT_TAB_SELECTED_EVENT + "_" + USER_CENTER_CATEGORY,
562            CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB,
563            CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB })
564    public void resetState() {
565        if (shouldResetStateOnTabChange) {
566            newUser = null;
567            selectedUser = null;
568            showUserOrGroup = false;
569            showCreateForm = false;
570            immediateCreation = false;
571            detailsMode = DETAILS_VIEW_MODE;
572        }
573    }
574
575    /**
576     * @return The type of creation for the user.
577     * @since 5.9.3
578     */
579    public boolean isImmediateCreation() {
580        return immediateCreation;
581    }
582
583    /**
584     * @param immediateCreation
585     * @since 5.9.3
586     */
587    public void setImmediateCreation(boolean immediateCreation) {
588        this.immediateCreation = immediateCreation;
589    }
590
591    public boolean isCreateAnotherUser() {
592        return createAnotherUser;
593    }
594
595    public void setCreateAnotherUser(boolean createAnotherUser) {
596        this.createAnotherUser = createAnotherUser;
597    }
598
599    public String getOldPassword() {
600        return oldPassword;
601    }
602
603    public void setOldPassword(String oldPassword) {
604        this.oldPassword = oldPassword;
605    }
606}