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