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}