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