/*
* (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Contributors:
* Nuxeo - initial API and implementation
*/
package org.nuxeo.ecm.webapp.security;
import static org.jboss.seam.ScopeType.APPLICATION;
import static org.jboss.seam.ScopeType.CONVERSATION;
import static org.jboss.seam.annotations.Install.FRAMEWORK;
import static org.nuxeo.ecm.platform.ui.web.api.WebActions.CURRENT_TAB_CHANGED_EVENT;
import static org.nuxeo.ecm.platform.ui.web.api.WebActions.CURRENT_TAB_SELECTED_EVENT;
import static org.nuxeo.ecm.user.invite.UserInvitationService.ValidationMethod.EMAIL;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.core.Events;
import org.jboss.seam.international.StatusMessage;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.repository.RepositoryManager;
import org.nuxeo.ecm.directory.BaseSession;
import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
import org.nuxeo.ecm.platform.usermanager.NuxeoPrincipalImpl;
import org.nuxeo.ecm.platform.usermanager.UserAdapter;
import org.nuxeo.ecm.platform.usermanager.UserAdapterImpl;
import org.nuxeo.ecm.platform.usermanager.exceptions.InvalidPasswordException;
import org.nuxeo.ecm.platform.usermanager.exceptions.UserAlreadyExistsException;
import org.nuxeo.ecm.user.invite.UserInvitationService;
import org.nuxeo.runtime.api.Framework;
/**
* Handles users management related web actions.
*
* @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
* @since 5.4.2
*/
@Name("userManagementActions")
@Scope(CONVERSATION)
@Install(precedence = FRAMEWORK)
public class UserManagementActions extends AbstractUserGroupManagement implements Serializable {
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(UserManagementActions.class);
public static final String USERS_TAB = USER_CENTER_CATEGORY + ":" + USERS_GROUPS_HOME + ":" + "UsersHome";
public static final String USERS_LISTING_CHANGED = "usersListingChanged";
public static final String USERS_SEARCH_CHANGED = "usersSearchChanged";
public static final String USER_SELECTED_CHANGED = "selectedUserChanged";
public static final String SELECTED_LETTER_CHANGED = "selectedLetterChanged";
protected String selectedLetter = "";
protected DocumentModel selectedUser;
protected DocumentModel newUser;
protected boolean immediateCreation = false;
protected boolean createAnotherUser = false;
protected String defaultRepositoryName = null;
protected String oldPassword;
@Override
protected String computeListingMode() {
return userManager.getUserListingMode();
}
public DocumentModel getSelectedUser() {
shouldResetStateOnTabChange = true;
return selectedUser;
}
public void setSelectedUser(DocumentModel user) {
fireSeamEvent(USER_SELECTED_CHANGED);
selectedUser = user;
}
/**
* @deprecated since version 5.5, use {@link #setSelectedUserName} instead.
*/
@Deprecated
public void setSelectedUser(String userName) {
setSelectedUser(refreshUser(userName));
}
/**
* UserRegistrationService userRegistrationService = Framework.getLocalService(UserRegistrationService.class);
*
* @since 5.5
*/
public void setSelectedUserName(String userName) {
setSelectedUser(refreshUser(userName));
}
public String getSelectedUserName() {
return selectedUser.getId();
}
// refresh to get references
protected DocumentModel refreshUser(String userName) {
return userManager.getUserModel(userName);
}
public String getSelectedLetter() {
return selectedLetter;
}
public void setSelectedLetter(String selectedLetter) {
if (selectedLetter != null && !selectedLetter.equals(this.selectedLetter)) {
this.selectedLetter = selectedLetter;
fireSeamEvent(SELECTED_LETTER_CHANGED);
}
this.selectedLetter = selectedLetter;
}
public DocumentModel getNewUser() {
if (newUser == null) {
newUser = userManager.getBareUserModel();
}
return newUser;
}
public boolean getAllowEditUser() {
return selectedUser != null && getCanEditUsers(true) && !BaseSession.isReadOnlyEntry(selectedUser);
}
protected boolean getCanEditUsers(boolean allowCurrentUser) {
if (userManager.areUsersReadOnly()) {
return false;
}
// if the selected user is the anonymous user, do not display
// edit/password tabs
if (selectedUser != null && userManager.getAnonymousUserId() != null
&& userManager.getAnonymousUserId().equals(selectedUser.getId())) {
return false;
}
if (selectedUser != null) {
NuxeoPrincipal selectedPrincipal = userManager.getPrincipal(selectedUser.getId());
if (selectedPrincipal.isAdministrator() && !((NuxeoPrincipal) currentUser).isAdministrator()) {
return false;
}
}
if (currentUser instanceof NuxeoPrincipal) {
NuxeoPrincipal pal = (NuxeoPrincipal) currentUser;
if (webActions.checkFilter(USERS_GROUPS_MANAGEMENT_ACCESS_FILTER)) {
return true;
}
if (allowCurrentUser && selectedUser != null) {
if (pal.getName().equals(selectedUser.getId())) {
return true;
}
}
}
return false;
}
public boolean getAllowChangePassword() {
return selectedUser != null && getCanEditUsers(true) && !BaseSession.isReadOnlyEntry(selectedUser);
}
public boolean getAllowCreateUser() {
return getCanEditUsers(false);
}
public boolean getAllowDeleteUser() {
return selectedUser != null && getCanEditUsers(false) && !BaseSession.isReadOnlyEntry(selectedUser);
}
public void clearSearch() {
searchString = null;
fireSeamEvent(USERS_SEARCH_CHANGED);
}
public void createUser() {
try {
if (immediateCreation) {
// Create the user with password
setSelectedUser(userManager.createUser(newUser));
// Set the default value for the creation
immediateCreation = false;
facesMessages.add(StatusMessage.Severity.INFO,
resourcesAccessor.getMessages().get("info.userManager.userCreated"));
if (createAnotherUser) {
showCreateForm = true;
} else {
showCreateForm = false;
showUserOrGroup = true;
detailsMode = null;
}
fireSeamEvent(USERS_LISTING_CHANGED);
} else {
UserInvitationService userRegistrationService = Framework.getLocalService(UserInvitationService.class);
Map<String, Serializable> additionalInfos = new HashMap<String, Serializable>();
// Wrap the form as an invitation to the user
UserAdapter newUserAdapter = new UserAdapterImpl(newUser, userManager);
DocumentModel userRegistrationDoc = wrapToUserRegistration(newUserAdapter);
userRegistrationService.submitRegistrationRequest(userRegistrationDoc, additionalInfos, EMAIL, true);
facesMessages.add(StatusMessage.Severity.INFO,
resourcesAccessor.getMessages().get("info.userManager.userInvited"));
if (createAnotherUser) {
showCreateForm = true;
} else {
showCreateForm = false;
showUserOrGroup = false;
detailsMode = null;
}
}
newUser = null;
} catch (UserAlreadyExistsException e) {
facesMessages.add(StatusMessage.Severity.ERROR,
resourcesAccessor.getMessages().get("error.userManager.userAlreadyExists"));
} catch (InvalidPasswordException e) {
facesMessages.add(StatusMessage.Severity.ERROR,
resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
} catch (Exception e) {
String message = e.getLocalizedMessage();
if (e.getCause() != null) {
message += e.getCause().getLocalizedMessage();
}
log.error(message, e);
facesMessages.add(StatusMessage.Severity.ERROR, message);
}
}
private String getDefaultRepositoryName() {
if (defaultRepositoryName == null) {
try {
defaultRepositoryName = Framework.getService(RepositoryManager.class).getDefaultRepository().getName();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return defaultRepositoryName;
}
public void updateUser() {
try {
UpdateUserUnrestricted runner = new UpdateUserUnrestricted(getDefaultRepositoryName(), selectedUser);
runner.runUnrestricted();
} catch (InvalidPasswordException e) {
facesMessages.add(StatusMessage.Severity.ERROR,
resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
}
detailsMode = DETAILS_VIEW_MODE;
fireSeamEvent(USERS_LISTING_CHANGED);
}
public String changePassword() {
try {
updateUser();
} catch (InvalidPasswordException e) {
facesMessages.add(StatusMessage.Severity.ERROR,
resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
return null;
}
detailsMode = DETAILS_VIEW_MODE;
String message = resourcesAccessor.getMessages().get("label.userManager.password.changed");
facesMessages.add(FacesMessage.SEVERITY_INFO, message);
fireSeamEvent(USERS_LISTING_CHANGED);
return null;
}
/**
* @since 8.2
*/
public String updateProfilePassword() {
if (userManager.checkUsernamePassword(currentUser.getName(), oldPassword)) {
try {
doAsSystemUser(new Runnable() {
@Override
public void run() {
userManager.updateUser(selectedUser);
}
});
} catch (InvalidPasswordException e) {
facesMessages.add(StatusMessage.Severity.ERROR,
resourcesAccessor.getMessages().get("error.userManager.invalidPassword"));
return null;
}
} else {
String message = resourcesAccessor.getMessages().get("label.userManager.old.password.error");
facesMessages.add(FacesMessage.SEVERITY_ERROR, message);
return null;
}
String message = resourcesAccessor.getMessages().get("label.userManager.password.changed");
facesMessages.add(FacesMessage.SEVERITY_INFO, message);
detailsMode = DETAILS_VIEW_MODE;
fireSeamEvent(USERS_LISTING_CHANGED);
return null;
}
protected void doAsSystemUser(Runnable runnable) {
LoginContext loginContext;
try {
loginContext = Framework.login();
} catch (LoginException e) {
throw new NuxeoException(e);
}
try {
runnable.run();
} finally {
try {
// Login context may be null in tests
if (loginContext != null) {
loginContext.logout();
}
} catch (LoginException e) {
throw new NuxeoException("Cannot log out system user", e);
}
}
}
public void deleteUser() {
userManager.deleteUser(selectedUser);
selectedUser = null;
showUserOrGroup = false;
fireSeamEvent(USERS_LISTING_CHANGED);
}
public void validateUserName(FacesContext context, UIComponent component, Object value) {
if (!(value instanceof String) || !StringUtils.containsOnly((String) value, VALID_CHARS)) {
FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
ComponentUtils.translate(context, "label.userManager.wrong.username"), null);
// also add global message
context.addMessage(null, message);
throw new ValidatorException(message);
}
}
/**
* Verify that only administrators can add administrator groups.
*
* @param context
* @param component
* @param value
* @since 5.9.2
*/
public void validateGroups(FacesContext context, UIComponent component, Object value) {
UIInput groupsComponent = getReferencedComponent("groupsValueHolderId", component);
@SuppressWarnings("unchecked")
List<String> groups = groupsComponent == null ? null : (List<String>) groupsComponent.getLocalValue();
if (groups == null || groups.isEmpty()) {
return;
}
if (!isAllowedToAdminGroups(groups)) {
throwValidationException(context, "label.userManager.invalidGroupSelected");
}
}
/**
* Checks if the current user is allowed to aministrate (meaning add/remove) the given groups.
*
* @param groups
* @return
* @since 5.9.2
*/
boolean isAllowedToAdminGroups(List<String> groups) {
NuxeoPrincipalImpl nuxeoPrincipal = (NuxeoPrincipalImpl) currentUser;
if (!nuxeoPrincipal.isAdministrator()) {
List<String> adminGroups = getAllAdminGroups();
for (String group : groups) {
if (adminGroups.contains(group)) {
return false;
}
}
}
return true;
}
/**
* Throw a validation exception with a translated message that is show in the UI.
*
* @param context the current faces context
* @param message the error message
* @param messageArgs the parameters for the message
* @since 5.9.2
*/
private void throwValidationException(FacesContext context, String message, Object... messageArgs) {
FacesMessage fmessage = new FacesMessage(FacesMessage.SEVERITY_ERROR,
ComponentUtils.translate(context, message, messageArgs), null);
throw new ValidatorException(fmessage);
}
/**
* Return the value of the JSF component who's id is references in an attribute of the componet passed in parameter.
*
* @param attribute the attribute holding the target component id
* @param component the component holding the attribute
* @return the UIInput component, null otherwise
* @since 5.9.2
*/
private UIInput getReferencedComponent(String attribute, UIComponent component) {
Map<String, Object> attributes = component.getAttributes();
String targetComponentId = (String) attributes.get(attribute);
if (targetComponentId == null) {
log.error(String.format("Target component id (%s) not found in attributes", attribute));
return null;
}
UIInput targetComponent = (UIInput) component.findComponent(targetComponentId);
if (targetComponent == null) {
return null;
}
return targetComponent;
}
public void validatePassword(FacesContext context, UIComponent component, Object value) {
Object firstPassword = getReferencedComponent("firstPasswordInputId", component).getLocalValue();
Object secondPassword = getReferencedComponent("secondPasswordInputId", component).getLocalValue();
if (firstPassword == null || secondPassword == null) {
log.error("Cannot validate passwords: value(s) not found");
return;
}
if (!firstPassword.equals(secondPassword)) {
throwValidationException(context, "label.userManager.password.not.match");
}
}
private DocumentModel wrapToUserRegistration(UserAdapter newUserAdapter) {
UserInvitationService userRegistrationService = Framework.getLocalService(UserInvitationService.class);
DocumentModel newUserRegistration = userRegistrationService.getUserRegistrationModel(null);
// Map the values from the object filled in the form
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoUsernameField(),
newUserAdapter.getName());
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoFirstnameField(),
newUserAdapter.getFirstName());
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoLastnameField(),
newUserAdapter.getLastName());
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoEmailField(),
newUserAdapter.getEmail());
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoGroupsField(),
newUserAdapter.getGroups().toArray());
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoTenantIdField(),
newUserAdapter.getTenantId());
newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoCompanyField(),
newUserAdapter.getCompany());
return newUserRegistration;
}
@Factory(value = "notReadOnly", scope = APPLICATION)
public boolean isNotReadOnly() {
return !Framework.isBooleanPropertyTrue("org.nuxeo.ecm.webapp.readonly.mode");
}
public List<String> getUserVirtualGroups(String userId) {
NuxeoPrincipal principal = userManager.getPrincipal(userId);
if (principal instanceof NuxeoPrincipalImpl) {
NuxeoPrincipalImpl user = (NuxeoPrincipalImpl) principal;
return user.getVirtualGroups();
}
return null;
}
public String viewUser(String userName) {
webActions.setCurrentTabIds(MAIN_TAB_HOME + "," + USERS_TAB);
setSelectedUser(userName);
setShowUser(Boolean.TRUE.toString());
return VIEW_HOME;
}
public String viewUser() {
if (selectedUser != null) {
return viewUser(selectedUser.getId());
} else {
return null;
}
}
/**
* @since 5.5
*/
public void setShowUser(String showUser) {
showUserOrGroup = Boolean.valueOf(showUser);
// do not reset the state before actually viewing the user
shouldResetStateOnTabChange = false;
}
protected void fireSeamEvent(String eventName) {
Events evtManager = Events.instance();
evtManager.raiseEvent(eventName);
}
@Factory(value = "anonymousUserDefined", scope = APPLICATION)
public boolean anonymousUserDefined() {
return userManager.getAnonymousUserId() != null;
}
@Observer(value = { USERS_LISTING_CHANGED })
public void onUsersListingChanged() {
contentViewActions.refreshOnSeamEvent(USERS_LISTING_CHANGED);
contentViewActions.resetPageProviderOnSeamEvent(USERS_LISTING_CHANGED);
}
@Observer(value = { USERS_SEARCH_CHANGED })
public void onUsersSearchChanged() {
contentViewActions.refreshOnSeamEvent(USERS_SEARCH_CHANGED);
contentViewActions.resetPageProviderOnSeamEvent(USERS_SEARCH_CHANGED);
}
@Observer(value = { SELECTED_LETTER_CHANGED })
public void onSelectedLetterChanged() {
contentViewActions.refreshOnSeamEvent(SELECTED_LETTER_CHANGED);
contentViewActions.resetPageProviderOnSeamEvent(SELECTED_LETTER_CHANGED);
}
@Observer(value = { CURRENT_TAB_CHANGED_EVENT + "_" + MAIN_TABS_CATEGORY,
CURRENT_TAB_CHANGED_EVENT + "_" + NUXEO_ADMIN_CATEGORY,
CURRENT_TAB_CHANGED_EVENT + "_" + USER_CENTER_CATEGORY,
CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB,
CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB,
CURRENT_TAB_SELECTED_EVENT + "_" + MAIN_TABS_CATEGORY,
CURRENT_TAB_SELECTED_EVENT + "_" + NUXEO_ADMIN_CATEGORY,
CURRENT_TAB_SELECTED_EVENT + "_" + USER_CENTER_CATEGORY,
CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB,
CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB })
public void resetState() {
if (shouldResetStateOnTabChange) {
newUser = null;
selectedUser = null;
showUserOrGroup = false;
showCreateForm = false;
immediateCreation = false;
detailsMode = DETAILS_VIEW_MODE;
}
}
/**
* @return The type of creation for the user.
* @since 5.9.3
*/
public boolean isImmediateCreation() {
return immediateCreation;
}
/**
* @param immediateCreation
* @since 5.9.3
*/
public void setImmediateCreation(boolean immediateCreation) {
this.immediateCreation = immediateCreation;
}
public boolean isCreateAnotherUser() {
return createAnotherUser;
}
public void setCreateAnotherUser(boolean createAnotherUser) {
this.createAnotherUser = createAnotherUser;
}
public String getOldPassword() {
return oldPassword;
}
public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
}
}