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