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