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