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