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:[email protected]">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() && !currentUser.isAdministrator()) { 182 return false; 183 } 184 } 185 186 if (webActions.checkFilter(USERS_GROUPS_MANAGEMENT_ACCESS_FILTER)) { 187 return true; 188 } 189 if (allowCurrentUser && selectedUser != null) { 190 if (currentUser.getName().equals(selectedUser.getId())) { 191 return true; 192 } 193 } 194 195 return false; 196 } 197 198 public boolean getAllowChangePassword() { 199 return selectedUser != null && getCanEditUsers(true) && !BaseSession.isReadOnlyEntry(selectedUser); 200 } 201 202 public boolean getAllowCreateUser() { 203 return getCanEditUsers(false); 204 } 205 206 public boolean getAllowDeleteUser() { 207 return selectedUser != null && getCanEditUsers(false) && !BaseSession.isReadOnlyEntry(selectedUser); 208 } 209 210 public void clearSearch() { 211 searchString = null; 212 fireSeamEvent(USERS_SEARCH_CHANGED); 213 } 214 215 public void createUser() { 216 try { 217 if (immediateCreation) { 218 // Create the user with password 219 setSelectedUser(userManager.createUser(newUser)); 220 // Set the default value for the creation 221 immediateCreation = false; 222 facesMessages.add(StatusMessage.Severity.INFO, 223 resourcesAccessor.getMessages().get("info.userManager.userCreated")); 224 if (createAnotherUser) { 225 showCreateForm = true; 226 } else { 227 showCreateForm = false; 228 showUserOrGroup = true; 229 detailsMode = null; 230 } 231 fireSeamEvent(USERS_LISTING_CHANGED); 232 } else { 233 UserInvitationService userRegistrationService = Framework.getService(UserInvitationService.class); 234 Map<String, Serializable> additionalInfos = new HashMap<String, Serializable>(); 235 additionalInfos.put(UserInvitationComponent.PARAM_ORIGINATING_USER , currentUser.getName()); 236 // Wrap the form as an invitation to the user 237 UserAdapter newUserAdapter = new UserAdapterImpl(newUser, userManager); 238 DocumentModel userRegistrationDoc = wrapToUserRegistration(newUserAdapter); 239 userRegistrationService.submitRegistrationRequest(userRegistrationDoc, additionalInfos, EMAIL, true); 240 241 facesMessages.add(StatusMessage.Severity.INFO, 242 resourcesAccessor.getMessages().get("info.userManager.userInvited")); 243 if (createAnotherUser) { 244 showCreateForm = true; 245 } else { 246 showCreateForm = false; 247 showUserOrGroup = false; 248 detailsMode = null; 249 } 250 251 } 252 newUser = null; 253 254 } catch (UserAlreadyExistsException e) { 255 facesMessages.add(StatusMessage.Severity.ERROR, 256 resourcesAccessor.getMessages().get("error.userManager.userAlreadyExists")); 257 } catch (InvalidPasswordException e) { 258 facesMessages.add(StatusMessage.Severity.ERROR, 259 resourcesAccessor.getMessages().get("error.userManager.invalidPassword")); 260 } catch (Exception e) { 261 String message = e.getLocalizedMessage(); 262 if (e.getCause() != null) { 263 message += e.getCause().getLocalizedMessage(); 264 } 265 log.error(message, e); 266 267 facesMessages.add(StatusMessage.Severity.ERROR, message); 268 269 } 270 } 271 272 private String getDefaultRepositoryName() { 273 if (defaultRepositoryName == null) { 274 try { 275 defaultRepositoryName = Framework.getService(RepositoryManager.class).getDefaultRepository().getName(); 276 } catch (Exception e) { 277 throw new RuntimeException(e); 278 } 279 } 280 return defaultRepositoryName; 281 } 282 283 public void updateUser() { 284 try { 285 UpdateUserUnrestricted runner = new UpdateUserUnrestricted(getDefaultRepositoryName(), selectedUser); 286 runner.runUnrestricted(); 287 } catch (InvalidPasswordException e) { 288 facesMessages.add(StatusMessage.Severity.ERROR, 289 resourcesAccessor.getMessages().get("error.userManager.invalidPassword")); 290 } 291 292 detailsMode = DETAILS_VIEW_MODE; 293 fireSeamEvent(USERS_LISTING_CHANGED); 294 } 295 296 public String changePassword() { 297 try { 298 updateUser(); 299 } catch (InvalidPasswordException e) { 300 facesMessages.add(StatusMessage.Severity.ERROR, 301 resourcesAccessor.getMessages().get("error.userManager.invalidPassword")); 302 return null; 303 } 304 detailsMode = DETAILS_VIEW_MODE; 305 306 String message = resourcesAccessor.getMessages().get("label.userManager.password.changed"); 307 facesMessages.add(FacesMessage.SEVERITY_INFO, message); 308 fireSeamEvent(USERS_LISTING_CHANGED); 309 310 return null; 311 } 312 313 /** 314 * @since 8.2 315 */ 316 public String updateProfilePassword() { 317 318 if (userManager.checkUsernamePassword(currentUser.getName(), oldPassword)) { 319 320 try { 321 Framework.doPrivileged(() -> userManager.updateUser(selectedUser)); 322 } catch (InvalidPasswordException e) { 323 facesMessages.add(StatusMessage.Severity.ERROR, 324 resourcesAccessor.getMessages().get("error.userManager.invalidPassword")); 325 return null; 326 } 327 } else { 328 String message = resourcesAccessor.getMessages().get("label.userManager.old.password.error"); 329 facesMessages.add(FacesMessage.SEVERITY_ERROR, message); 330 return null; 331 } 332 333 String message = resourcesAccessor.getMessages().get("label.userManager.password.changed"); 334 facesMessages.add(FacesMessage.SEVERITY_INFO, message); 335 detailsMode = DETAILS_VIEW_MODE; 336 fireSeamEvent(USERS_LISTING_CHANGED); 337 338 return null; 339 } 340 341 public void deleteUser() { 342 userManager.deleteUser(selectedUser); 343 selectedUser = null; 344 showUserOrGroup = false; 345 fireSeamEvent(USERS_LISTING_CHANGED); 346 } 347 348 public void validateUserName(FacesContext context, UIComponent component, Object value) { 349 if (!(value instanceof String) || !StringUtils.containsOnly((String) value, VALID_CHARS)) { 350 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, 351 ComponentUtils.translate(context, "label.userManager.wrong.username"), null); 352 // also add global message 353 context.addMessage(null, message); 354 throw new ValidatorException(message); 355 } 356 } 357 358 /** 359 * Verify that only administrators can add administrator groups. 360 * 361 * @param context 362 * @param component 363 * @param value 364 * @since 5.9.2 365 */ 366 public void validateGroups(FacesContext context, UIComponent component, Object value) { 367 368 UIInput groupsComponent = getReferencedComponent("groupsValueHolderId", component); 369 370 @SuppressWarnings("unchecked") 371 List<String> groups = groupsComponent == null ? null : (List<String>) groupsComponent.getLocalValue(); 372 if (groups == null || groups.isEmpty()) { 373 return; 374 } 375 if (!isAllowedToAdminGroups(groups)) { 376 throwValidationException(context, "label.userManager.invalidGroupSelected"); 377 } 378 } 379 380 /** 381 * Checks if the current user is allowed to aministrate (meaning add/remove) the given groups. 382 * 383 * @param groups 384 * @return 385 * @since 5.9.2 386 */ 387 boolean isAllowedToAdminGroups(List<String> groups) { 388 NuxeoPrincipalImpl nuxeoPrincipal = (NuxeoPrincipalImpl) currentUser; 389 390 if (!nuxeoPrincipal.isAdministrator()) { 391 List<String> adminGroups = getAllAdminGroups(); 392 393 for (String group : groups) { 394 if (adminGroups.contains(group)) { 395 return false; 396 } 397 } 398 399 } 400 return true; 401 } 402 403 /** 404 * Throw a validation exception with a translated message that is show in the UI. 405 * 406 * @param context the current faces context 407 * @param message the error message 408 * @param messageArgs the parameters for the message 409 * @since 5.9.2 410 */ 411 private void throwValidationException(FacesContext context, String message, Object... messageArgs) { 412 FacesMessage fmessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, 413 ComponentUtils.translate(context, message, messageArgs), null); 414 throw new ValidatorException(fmessage); 415 } 416 417 /** 418 * Return the value of the JSF component who's id is references in an attribute of the componet passed in parameter. 419 * 420 * @param attribute the attribute holding the target component id 421 * @param component the component holding the attribute 422 * @return the UIInput component, null otherwise 423 * @since 5.9.2 424 */ 425 private UIInput getReferencedComponent(String attribute, UIComponent component) { 426 Map<String, Object> attributes = component.getAttributes(); 427 String targetComponentId = (String) attributes.get(attribute); 428 429 if (targetComponentId == null) { 430 log.error(String.format("Target component id (%s) not found in attributes", attribute)); 431 return null; 432 } 433 434 UIInput targetComponent = (UIInput) component.findComponent(targetComponentId); 435 if (targetComponent == null) { 436 return null; 437 } 438 439 return targetComponent; 440 } 441 442 public void validatePassword(FacesContext context, UIComponent component, Object value) { 443 444 Object firstPassword = getReferencedComponent("firstPasswordInputId", component).getLocalValue(); 445 Object secondPassword = getReferencedComponent("secondPasswordInputId", component).getLocalValue(); 446 447 if (firstPassword == null || secondPassword == null) { 448 log.error("Cannot validate passwords: value(s) not found"); 449 return; 450 } 451 452 if (!firstPassword.equals(secondPassword)) { 453 throwValidationException(context, "label.userManager.password.not.match"); 454 } 455 456 } 457 458 private DocumentModel wrapToUserRegistration(UserAdapter newUserAdapter) { 459 UserInvitationService userRegistrationService = Framework.getService(UserInvitationService.class); 460 DocumentModel newUserRegistration = userRegistrationService.getUserRegistrationModel(null); 461 462 // Map the values from the object filled in the form 463 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoUsernameField(), 464 newUserAdapter.getName()); 465 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoFirstnameField(), 466 newUserAdapter.getFirstName()); 467 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoLastnameField(), 468 newUserAdapter.getLastName()); 469 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoEmailField(), 470 newUserAdapter.getEmail()); 471 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoGroupsField(), 472 newUserAdapter.getGroups().toArray()); 473 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoCompanyField(), 474 newUserAdapter.getCompany()); 475 476 String tenantId = newUserAdapter.getTenantId(); 477 if (StringUtils.isBlank(tenantId)) { 478 tenantId = currentUser.getTenantId(); 479 } 480 newUserRegistration.setPropertyValue(userRegistrationService.getConfiguration().getUserInfoTenantIdField(), 481 tenantId); 482 483 return newUserRegistration; 484 } 485 486 @Factory(value = "notReadOnly", scope = APPLICATION) 487 public boolean isNotReadOnly() { 488 return !Framework.isBooleanPropertyTrue("org.nuxeo.ecm.webapp.readonly.mode"); 489 } 490 491 public List<String> getUserVirtualGroups(String userId) { 492 NuxeoPrincipal principal = userManager.getPrincipal(userId); 493 if (principal instanceof NuxeoPrincipalImpl) { 494 NuxeoPrincipalImpl user = (NuxeoPrincipalImpl) principal; 495 return user.getVirtualGroups(); 496 } 497 return null; 498 } 499 500 public String viewUser(String userName) { 501 webActions.setCurrentTabIds(MAIN_TAB_HOME + "," + USERS_TAB); 502 setSelectedUser(userName); 503 setShowUser(Boolean.TRUE.toString()); 504 return VIEW_HOME; 505 } 506 507 public String viewUser() { 508 if (selectedUser != null) { 509 return viewUser(selectedUser.getId()); 510 } else { 511 return null; 512 } 513 } 514 515 /** 516 * @since 5.5 517 */ 518 public void setShowUser(String showUser) { 519 showUserOrGroup = Boolean.valueOf(showUser); 520 // do not reset the state before actually viewing the user 521 shouldResetStateOnTabChange = false; 522 } 523 524 protected void fireSeamEvent(String eventName) { 525 Events evtManager = Events.instance(); 526 evtManager.raiseEvent(eventName); 527 } 528 529 @Factory(value = "anonymousUserDefined", scope = APPLICATION) 530 public boolean anonymousUserDefined() { 531 return userManager.getAnonymousUserId() != null; 532 } 533 534 @Observer(value = { USERS_LISTING_CHANGED }) 535 public void onUsersListingChanged() { 536 contentViewActions.refreshOnSeamEvent(USERS_LISTING_CHANGED); 537 contentViewActions.resetPageProviderOnSeamEvent(USERS_LISTING_CHANGED); 538 } 539 540 @Observer(value = { USERS_SEARCH_CHANGED }) 541 public void onUsersSearchChanged() { 542 contentViewActions.refreshOnSeamEvent(USERS_SEARCH_CHANGED); 543 contentViewActions.resetPageProviderOnSeamEvent(USERS_SEARCH_CHANGED); 544 } 545 546 @Observer(value = { SELECTED_LETTER_CHANGED }) 547 public void onSelectedLetterChanged() { 548 contentViewActions.refreshOnSeamEvent(SELECTED_LETTER_CHANGED); 549 contentViewActions.resetPageProviderOnSeamEvent(SELECTED_LETTER_CHANGED); 550 } 551 552 @Observer(value = { CURRENT_TAB_CHANGED_EVENT + "_" + MAIN_TABS_CATEGORY, 553 CURRENT_TAB_CHANGED_EVENT + "_" + NUXEO_ADMIN_CATEGORY, 554 CURRENT_TAB_CHANGED_EVENT + "_" + USER_CENTER_CATEGORY, 555 CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB, 556 CURRENT_TAB_CHANGED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB, 557 CURRENT_TAB_SELECTED_EVENT + "_" + MAIN_TABS_CATEGORY, 558 CURRENT_TAB_SELECTED_EVENT + "_" + NUXEO_ADMIN_CATEGORY, 559 CURRENT_TAB_SELECTED_EVENT + "_" + USER_CENTER_CATEGORY, 560 CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_MANAGER_SUB_TAB, 561 CURRENT_TAB_SELECTED_EVENT + "_" + USERS_GROUPS_HOME_SUB_TAB }) 562 public void resetState() { 563 if (shouldResetStateOnTabChange) { 564 newUser = null; 565 selectedUser = null; 566 showUserOrGroup = false; 567 showCreateForm = false; 568 immediateCreation = false; 569 detailsMode = DETAILS_VIEW_MODE; 570 } 571 } 572 573 /** 574 * @return The type of creation for the user. 575 * @since 5.9.3 576 */ 577 public boolean isImmediateCreation() { 578 return immediateCreation; 579 } 580 581 /** 582 * @param immediateCreation 583 * @since 5.9.3 584 */ 585 public void setImmediateCreation(boolean immediateCreation) { 586 this.immediateCreation = immediateCreation; 587 } 588 589 public boolean isCreateAnotherUser() { 590 return createAnotherUser; 591 } 592 593 public void setCreateAnotherUser(boolean createAnotherUser) { 594 this.createAnotherUser = createAnotherUser; 595 } 596 597 public String getOldPassword() { 598 return oldPassword; 599 } 600 601 public void setOldPassword(String oldPassword) { 602 this.oldPassword = oldPassword; 603 } 604}