001/* 002 * (C) Copyright 2006-2018 Nuxeo (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 * 016 * Contributors: 017 * George Lefter 018 * Florent Guillaume 019 * Anahide Tchertchian 020 * Gagnavarslan ehf 021 */ 022package org.nuxeo.ecm.platform.usermanager; 023 024import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; 025import static org.nuxeo.ecm.platform.usermanager.UserConfig.GROUPS_COLUMN; 026 027import java.io.Serializable; 028import java.security.Principal; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.HashSet; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.Set; 039import java.util.regex.Matcher; 040import java.util.regex.Pattern; 041 042import org.apache.commons.codec.digest.DigestUtils; 043import org.apache.commons.lang3.StringUtils; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.nuxeo.ecm.core.api.DocumentModel; 047import org.nuxeo.ecm.core.api.DocumentModelComparator; 048import org.nuxeo.ecm.core.api.DocumentModelList; 049import org.nuxeo.ecm.core.api.NuxeoException; 050import org.nuxeo.ecm.core.api.NuxeoGroup; 051import org.nuxeo.ecm.core.api.NuxeoPrincipal; 052import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 053import org.nuxeo.ecm.core.api.local.ClientLoginModule; 054import org.nuxeo.ecm.core.api.model.Property; 055import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 056import org.nuxeo.ecm.core.api.security.ACE; 057import org.nuxeo.ecm.core.api.security.ACL; 058import org.nuxeo.ecm.core.api.security.ACP; 059import org.nuxeo.ecm.core.api.security.AdministratorGroupsProvider; 060import org.nuxeo.ecm.core.api.security.PermissionProvider; 061import org.nuxeo.ecm.core.api.security.SecurityConstants; 062import org.nuxeo.ecm.core.cache.Cache; 063import org.nuxeo.ecm.core.cache.CacheService; 064import org.nuxeo.ecm.core.event.EventProducer; 065import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 066import org.nuxeo.ecm.core.event.impl.UnboundEventContext; 067import org.nuxeo.ecm.core.query.sql.model.MultiExpression; 068import org.nuxeo.ecm.core.query.sql.model.Operator; 069import org.nuxeo.ecm.core.query.sql.model.OrderByExpr; 070import org.nuxeo.ecm.core.query.sql.model.OrderByExprs; 071import org.nuxeo.ecm.core.query.sql.model.OrderByList; 072import org.nuxeo.ecm.core.query.sql.model.Predicate; 073import org.nuxeo.ecm.core.query.sql.model.Predicates; 074import org.nuxeo.ecm.core.query.sql.model.QueryBuilder; 075import org.nuxeo.ecm.directory.AbstractDirectory; 076import org.nuxeo.ecm.directory.BaseDirectoryDescriptor.SubstringMatchType; 077import org.nuxeo.ecm.directory.BaseSession; 078import org.nuxeo.ecm.directory.Directory; 079import org.nuxeo.ecm.directory.DirectoryException; 080import org.nuxeo.ecm.directory.Session; 081import org.nuxeo.ecm.directory.api.DirectoryService; 082import org.nuxeo.ecm.directory.memory.MemoryDirectoryExpressionEvaluator; 083import org.nuxeo.ecm.platform.usermanager.exceptions.GroupAlreadyExistsException; 084import org.nuxeo.ecm.platform.usermanager.exceptions.InvalidPasswordException; 085import org.nuxeo.ecm.platform.usermanager.exceptions.UserAlreadyExistsException; 086import org.nuxeo.runtime.api.Framework; 087import org.nuxeo.runtime.services.config.ConfigurationService; 088import org.nuxeo.runtime.services.event.Event; 089import org.nuxeo.runtime.services.event.EventService; 090 091/** 092 * Standard implementation of the Nuxeo UserManager. 093 */ 094public class UserManagerImpl implements UserManager, MultiTenantUserManager, AdministratorGroupsProvider { 095 096 private static final String VALIDATE_PASSWORD_PARAM = "nuxeo.usermanager.check.password"; 097 098 private static final long serialVersionUID = 1L; 099 100 private static final Log log = LogFactory.getLog(UserManagerImpl.class); 101 102 public static final String USERMANAGER_TOPIC = "usermanager"; 103 104 /** Used by JaasCacheFlusher. */ 105 public static final String USERCHANGED_EVENT_ID = "user_changed"; 106 107 public static final String USERCREATED_EVENT_ID = "user_created"; 108 109 public static final String USERDELETED_EVENT_ID = "user_deleted"; 110 111 public static final String USERMODIFIED_EVENT_ID = "user_modified"; 112 113 /** Used by JaasCacheFlusher. */ 114 public static final String GROUPCHANGED_EVENT_ID = "group_changed"; 115 116 public static final String GROUPCREATED_EVENT_ID = "group_created"; 117 118 public static final String GROUPDELETED_EVENT_ID = "group_deleted"; 119 120 public static final String GROUPMODIFIED_EVENT_ID = "group_modified"; 121 122 public static final String DEFAULT_ANONYMOUS_USER_ID = "Anonymous"; 123 124 public static final String VIRTUAL_FIELD_FILTER_PREFIX = "__"; 125 126 public static final String INVALIDATE_PRINCIPAL_EVENT_ID = "invalidatePrincipal"; 127 128 public static final String INVALIDATE_ALL_PRINCIPALS_EVENT_ID = "invalidateAllPrincipals"; 129 130 /** 131 * Possible value for the {@link DocumentEventContext#CATEGORY_PROPERTY_KEY} key of a core event context. 132 * 133 * @since 9.2 134 */ 135 public static final String USER_GROUP_CATEGORY = "userGroup"; 136 137 /** 138 * Key for the id of a user or a group in a core event context. 139 * 140 * @since 9.2 141 */ 142 public static final String ID_PROPERTY_KEY = "id"; 143 144 /** 145 * Key for the ancestor group names of a group in a core event context. 146 * 147 * @since 9.2 148 */ 149 public static final String ANCESTOR_GROUPS_PROPERTY_KEY = "ancestorGroups"; 150 151 protected final DirectoryService dirService; 152 153 protected final CacheService cacheService; 154 155 protected Cache principalCache = null; 156 157 public UserMultiTenantManagement multiTenantManagement = new DefaultUserMultiTenantManagement(); 158 159 /** 160 * A structure used to inject field name configuration of users schema into a NuxeoPrincipalImpl instance. TODO not 161 * all fields inside are configurable for now - they will use default values 162 */ 163 protected UserConfig userConfig; 164 165 /** 166 * @since 9.3 167 */ 168 protected GroupConfig groupConfig; 169 170 protected String userDirectoryName; 171 172 protected String userSchemaName; 173 174 protected String userIdField; 175 176 protected String userEmailField; 177 178 protected Map<String, MatchType> userSearchFields; 179 180 protected String groupDirectoryName; 181 182 protected String groupSchemaName; 183 184 protected String groupIdField; 185 186 protected String groupLabelField; 187 188 protected String groupMembersField; 189 190 protected String groupSubGroupsField; 191 192 protected String groupParentGroupsField; 193 194 protected String groupSortField; 195 196 protected Map<String, MatchType> groupSearchFields; 197 198 protected String defaultGroup; 199 200 protected List<String> administratorIds; 201 202 protected List<String> administratorGroups; 203 204 protected Boolean disableDefaultAdministratorsGroup; 205 206 protected String userSortField; 207 208 protected String userListingMode; 209 210 protected String groupListingMode; 211 212 protected Pattern userPasswordPattern; 213 214 protected VirtualUser anonymousUser; 215 216 protected String digestAuthDirectory; 217 218 protected String digestAuthRealm; 219 220 protected final Map<String, VirtualUserDescriptor> virtualUsers; 221 222 public UserManagerImpl() { 223 dirService = Framework.getService(DirectoryService.class); 224 cacheService = Framework.getService(CacheService.class); 225 virtualUsers = new HashMap<>(); 226 userConfig = new UserConfig(); 227 } 228 229 @Override 230 public void setConfiguration(UserManagerDescriptor descriptor) { 231 defaultGroup = descriptor.defaultGroup; 232 administratorIds = descriptor.defaultAdministratorIds; 233 disableDefaultAdministratorsGroup = false; 234 if (descriptor.disableDefaultAdministratorsGroup != null) { 235 disableDefaultAdministratorsGroup = descriptor.disableDefaultAdministratorsGroup; 236 } 237 administratorGroups = new ArrayList<>(); 238 if (!disableDefaultAdministratorsGroup) { 239 administratorGroups.add(SecurityConstants.ADMINISTRATORS); 240 } 241 if (descriptor.administratorsGroups != null) { 242 administratorGroups.addAll(descriptor.administratorsGroups); 243 } 244 if (administratorGroups.isEmpty()) { 245 log.warn("No administrators group has been defined: at least one should be set" 246 + " to avoid lockups when blocking rights for instance"); 247 } 248 userSortField = descriptor.userSortField; 249 groupSortField = descriptor.groupSortField; 250 userListingMode = descriptor.userListingMode; 251 groupListingMode = descriptor.groupListingMode; 252 userEmailField = descriptor.userEmailField; 253 userSearchFields = descriptor.userSearchFields; 254 userPasswordPattern = descriptor.userPasswordPattern; 255 groupLabelField = descriptor.groupLabelField; 256 groupMembersField = descriptor.groupMembersField; 257 groupSubGroupsField = descriptor.groupSubGroupsField; 258 groupParentGroupsField = descriptor.groupParentGroupsField; 259 groupSearchFields = descriptor.groupSearchFields; 260 anonymousUser = descriptor.anonymousUser; 261 262 setUserDirectoryName(descriptor.userDirectoryName); 263 setGroupDirectoryName(descriptor.groupDirectoryName); 264 setVirtualUsers(descriptor.virtualUsers); 265 266 digestAuthDirectory = descriptor.digestAuthDirectory; 267 digestAuthRealm = descriptor.digestAuthRealm; 268 269 userConfig = new UserConfig(); 270 userConfig.emailKey = userEmailField; 271 userConfig.schemaName = userSchemaName; 272 userConfig.nameKey = userIdField; 273 274 groupConfig = new GroupConfig(); 275 groupConfig.schemaName = groupSchemaName; 276 groupConfig.idField = groupIdField; 277 groupConfig.labelField = groupLabelField; 278 groupConfig.membersField = groupMembersField; 279 groupConfig.subGroupsField = groupSubGroupsField; 280 groupConfig.parentGroupsField = groupParentGroupsField; 281 282 if (cacheService != null && descriptor.userCacheName != null) { 283 principalCache = cacheService.getCache(descriptor.userCacheName); 284 invalidateAllPrincipals(); 285 } 286 287 } 288 289 protected void setUserDirectoryName(String userDirectoryName) { 290 this.userDirectoryName = userDirectoryName; 291 userSchemaName = dirService.getDirectorySchema(userDirectoryName); 292 userIdField = dirService.getDirectoryIdField(userDirectoryName); 293 } 294 295 @Override 296 public String getUserDirectoryName() { 297 return userDirectoryName; 298 } 299 300 @Override 301 public String getUserIdField() { 302 return userIdField; 303 } 304 305 @Override 306 public String getUserSchemaName() { 307 return userSchemaName; 308 } 309 310 @Override 311 public String getUserEmailField() { 312 return userEmailField; 313 } 314 315 @Override 316 public Set<String> getUserSearchFields() { 317 return Collections.unmodifiableSet(userSearchFields.keySet()); 318 } 319 320 @Override 321 public Set<String> getGroupSearchFields() { 322 return Collections.unmodifiableSet(groupSearchFields.keySet()); 323 } 324 325 protected void setGroupDirectoryName(String groupDirectoryName) { 326 this.groupDirectoryName = groupDirectoryName; 327 groupSchemaName = dirService.getDirectorySchema(groupDirectoryName); 328 groupIdField = dirService.getDirectoryIdField(groupDirectoryName); 329 } 330 331 @Override 332 public String getGroupDirectoryName() { 333 return groupDirectoryName; 334 } 335 336 @Override 337 public String getGroupIdField() { 338 return groupIdField; 339 } 340 341 @Override 342 public String getGroupLabelField() { 343 return groupLabelField; 344 } 345 346 @Override 347 public String getGroupSchemaName() { 348 return groupSchemaName; 349 } 350 351 @Override 352 public String getGroupMembersField() { 353 return groupMembersField; 354 } 355 356 @Override 357 public String getGroupSubGroupsField() { 358 return groupSubGroupsField; 359 } 360 361 @Override 362 public String getGroupParentGroupsField() { 363 return groupParentGroupsField; 364 } 365 366 @Override 367 public String getUserListingMode() { 368 return userListingMode; 369 } 370 371 @Override 372 public String getGroupListingMode() { 373 return groupListingMode; 374 } 375 376 @Override 377 public String getDefaultGroup() { 378 return defaultGroup; 379 } 380 381 @Override 382 public Pattern getUserPasswordPattern() { 383 return userPasswordPattern; 384 } 385 386 @Override 387 public String getAnonymousUserId() { 388 if (anonymousUser == null) { 389 return null; 390 } 391 String anonymousUserId = anonymousUser.getId(); 392 if (anonymousUserId == null) { 393 return DEFAULT_ANONYMOUS_USER_ID; 394 } 395 return anonymousUserId; 396 } 397 398 protected void setVirtualUsers(Map<String, VirtualUserDescriptor> virtualUsers) { 399 this.virtualUsers.clear(); 400 if (virtualUsers != null) { 401 this.virtualUsers.putAll(virtualUsers); 402 } 403 } 404 405 @Override 406 public boolean checkUsernamePassword(String username, String password) { 407 408 if (username == null || password == null) { 409 log.warn("Trying to authenticate against null username or password"); 410 return false; 411 } 412 413 // deal with anonymous user 414 String anonymousUserId = getAnonymousUserId(); 415 if (username.equals(anonymousUserId)) { 416 log.warn(String.format("Trying to authenticate anonymous user (%s)", anonymousUserId)); 417 return false; 418 } 419 420 // deal with virtual users 421 if (virtualUsers.containsKey(username)) { 422 VirtualUser user = virtualUsers.get(username); 423 String expected = user.getPassword(); 424 if (expected == null) { 425 return false; 426 } 427 return expected.equals(password); 428 } 429 430 String userDirName; 431 // BBB backward compat for userDirectory + userAuthentication 432 if ("userDirectory".equals(userDirectoryName) && dirService.getDirectory("userAuthentication") != null) { 433 userDirName = "userAuthentication"; 434 } else { 435 userDirName = userDirectoryName; 436 } 437 try (Session userDir = dirService.open(userDirName)) { 438 if (!userDir.isAuthenticating()) { 439 log.error("Trying to authenticate against a non authenticating " + "directory: " + userDirName); 440 return false; 441 } 442 443 boolean authenticated = userDir.authenticate(username, password); 444 if (authenticated) { 445 Framework.doPrivileged(() -> syncDigestAuthPassword(username, password)); 446 } 447 return authenticated; 448 } 449 } 450 451 protected void syncDigestAuthPassword(String username, String password) { 452 if (StringUtils.isEmpty(digestAuthDirectory) || StringUtils.isEmpty(digestAuthRealm) || username == null 453 || password == null) { 454 return; 455 } 456 457 String ha1 = encodeDigestAuthPassword(username, digestAuthRealm, password); 458 try (Session dir = dirService.open(digestAuthDirectory)) { 459 dir.setReadAllColumns(true); // needed to read digest password 460 String schema = dirService.getDirectorySchema(digestAuthDirectory); 461 DocumentModel entry = dir.getEntry(username, true); 462 if (entry == null) { 463 entry = getDigestAuthModel(); 464 entry.setProperty(schema, dir.getIdField(), username); 465 entry.setProperty(schema, dir.getPasswordField(), ha1); 466 dir.createEntry(entry); 467 log.debug("Created digest auth password for user:" + username); 468 } else { 469 String storedHa1 = (String) entry.getProperty(schema, dir.getPasswordField()); 470 if (!ha1.equals(storedHa1)) { 471 entry.setProperty(schema, dir.getPasswordField(), ha1); 472 dir.updateEntry(entry); 473 log.debug("Updated digest auth password for user:" + username); 474 } 475 } 476 } catch (DirectoryException e) { 477 log.warn("Digest auth password not synchronized, check your configuration", e); 478 } 479 } 480 481 protected DocumentModel getDigestAuthModel() { 482 String schema = dirService.getDirectorySchema(digestAuthDirectory); 483 return BaseSession.createEntryModel(null, schema, null, null); 484 } 485 486 public static String encodeDigestAuthPassword(String username, String realm, String password) { 487 String a1 = username + ":" + realm + ":" + password; 488 return DigestUtils.md5Hex(a1); 489 } 490 491 @Override 492 public String getDigestAuthDirectory() { 493 return digestAuthDirectory; 494 } 495 496 @Override 497 public String getDigestAuthRealm() { 498 return digestAuthRealm; 499 } 500 501 @Override 502 public boolean validatePassword(String password) { 503 if (userPasswordPattern == null) { 504 return true; 505 } else { 506 Matcher userPasswordMatcher = userPasswordPattern.matcher(password); 507 return userPasswordMatcher.find(); 508 } 509 } 510 511 protected NuxeoPrincipal makeAnonymousPrincipal() { 512 DocumentModel userEntry = makeVirtualUserEntry(getAnonymousUserId(), anonymousUser); 513 // XXX: pass anonymous user groups, but they will be ignored 514 return makePrincipal(userEntry, true, anonymousUser.getGroups()); 515 } 516 517 protected NuxeoPrincipal makeVirtualPrincipal(VirtualUser user) { 518 DocumentModel userEntry = makeVirtualUserEntry(user.getId(), user); 519 return makePrincipal(userEntry, false, user.getGroups()); 520 } 521 522 protected NuxeoPrincipal makeTransientPrincipal(String username) { 523 DocumentModel userEntry = BaseSession.createEntryModel(null, userSchemaName, username, null); 524 userEntry.setProperty(userSchemaName, userIdField, username); 525 NuxeoPrincipal principal = makePrincipal(userEntry, false, true, null); 526 String[] parts = username.split("/"); 527 String email = parts[1]; 528 principal.setFirstName(email); 529 principal.setEmail(email); 530 return principal; 531 } 532 533 protected DocumentModel makeVirtualUserEntry(String id, VirtualUser user) { 534 final DocumentModel userEntry = BaseSession.createEntryModel(null, userSchemaName, id, null); 535 // at least fill id field 536 userEntry.setProperty(userSchemaName, userIdField, id); 537 for (Entry<String, Serializable> prop : user.getProperties().entrySet()) { 538 try { 539 userEntry.setProperty(userSchemaName, prop.getKey(), prop.getValue()); 540 } catch (PropertyNotFoundException ce) { 541 log.error("Property: " + prop.getKey() + " does not exists. Check your " + "UserService configuration.", 542 ce); 543 } 544 } 545 return userEntry; 546 } 547 548 protected NuxeoPrincipal makePrincipal(DocumentModel userEntry) { 549 return makePrincipal(userEntry, false, null); 550 } 551 552 protected NuxeoPrincipal makePrincipal(DocumentModel userEntry, boolean anonymous, List<String> groups) { 553 return makePrincipal(userEntry, anonymous, false, groups); 554 } 555 556 protected NuxeoPrincipal makePrincipal(DocumentModel userEntry, boolean anonymous, boolean isTransient, 557 List<String> groups) { 558 boolean admin = false; 559 String username = userEntry.getId(); 560 561 List<String> virtualGroups = new LinkedList<>(); 562 // Add preconfigured groups: useful for LDAP, not for anonymous users 563 if (defaultGroup != null && !anonymous && !isTransient) { 564 virtualGroups.add(defaultGroup); 565 } 566 // Add additional groups: useful for virtual users 567 if (groups != null && !isTransient) { 568 virtualGroups.addAll(groups); 569 } 570 // Create a default admin if needed 571 if (administratorIds != null && administratorIds.contains(username)) { 572 admin = true; 573 if (administratorGroups != null) { 574 virtualGroups.addAll(administratorGroups); 575 } 576 } 577 578 NuxeoPrincipalImpl principal = new NuxeoPrincipalImpl(username, anonymous, admin, false); 579 principal.setConfig(userConfig); 580 581 principal.setModel(userEntry, false); 582 principal.setVirtualGroups(virtualGroups, true); 583 584 // TODO: reenable roles initialization once we have a use case for 585 // a role directory. In the mean time we only set the JBOSS role 586 // that is required to login 587 List<String> roles = Collections.singletonList("regular"); 588 principal.setRoles(roles); 589 590 return principal; 591 } 592 593 protected boolean useCache() { 594 return principalCache != null; 595 } 596 597 @Override 598 public NuxeoPrincipal getPrincipal(String username) { 599 if (useCache()) { 600 return getPrincipalUsingCache(username); 601 } 602 return getPrincipal(username, null); 603 } 604 605 protected NuxeoPrincipal getPrincipalUsingCache(String username) { 606 NuxeoPrincipal ret = (NuxeoPrincipal) principalCache.get(username); 607 if (ret == null) { 608 ret = getPrincipal(username, null); 609 if (ret == null) { 610 return ret; 611 } 612 principalCache.put(username, ret); 613 } 614 return ((NuxeoPrincipalImpl) ret).cloneTransferable(); // should not return cached principal 615 } 616 617 @Override 618 public DocumentModel getUserModel(String userName) { 619 return getUserModel(userName, null); 620 } 621 622 @Override 623 public DocumentModel getBareUserModel() { 624 String schema = dirService.getDirectorySchema(userDirectoryName); 625 return BaseSession.createEntryModel(null, schema, null, null); 626 } 627 628 @Override 629 public NuxeoGroup getGroup(String groupName) { 630 return getGroup(groupName, null); 631 } 632 633 protected NuxeoGroup getGroup(String groupName, DocumentModel context) { 634 DocumentModel groupEntry = getGroupModel(groupName, context); 635 if (groupEntry != null) { 636 return makeGroup(groupEntry); 637 } 638 return null; 639 640 } 641 642 @Override 643 public DocumentModel getGroupModel(String groupName) { 644 return getGroupModel(groupName, null); 645 } 646 647 @SuppressWarnings("unchecked") 648 protected NuxeoGroup makeGroup(DocumentModel groupEntry) { 649 return new NuxeoGroupImpl(groupEntry, groupConfig); 650 } 651 652 @Override 653 public List<String> getTopLevelGroups() { 654 return getTopLevelGroups(null); 655 } 656 657 @Override 658 public List<String> getGroupsInGroup(String parentId) { 659 NuxeoGroup group = getGroup(parentId, null); 660 if (group != null) { 661 return group.getMemberGroups(); 662 } else { 663 return Collections.emptyList(); 664 } 665 } 666 667 @Override 668 public List<String> getUsersInGroup(String groupId) { 669 return getGroup(groupId).getMemberUsers(); 670 } 671 672 @Override 673 public List<String> getUsersInGroupAndSubGroups(String groupId) { 674 return getUsersInGroupAndSubGroups(groupId, null); 675 } 676 677 protected void appendSubgroups(String groupId, Set<String> groups, DocumentModel context) { 678 List<String> groupsToAppend = getGroupsInGroup(groupId, context); 679 groups.addAll(groupsToAppend); 680 for (String subgroupId : groupsToAppend) { 681 groups.add(subgroupId); 682 // avoiding infinite loop 683 if (!groups.contains(subgroupId)) { 684 appendSubgroups(subgroupId, groups, context); 685 } 686 } 687 688 } 689 690 protected boolean isAnonymousMatching(Map<String, Serializable> filter, Set<String> fulltext) { 691 String anonymousUserId = getAnonymousUserId(); 692 if (anonymousUserId == null) { 693 return false; 694 } 695 if (filter == null || filter.isEmpty()) { 696 return true; 697 } 698 Map<String, Serializable> anonymousUserMap = anonymousUser.getProperties(); 699 anonymousUserMap.put(userIdField, anonymousUserId); 700 for (Entry<String, Serializable> e : filter.entrySet()) { 701 String fieldName = e.getKey(); 702 Object expected = e.getValue(); 703 Object value = anonymousUserMap.get(fieldName); 704 if (value == null) { 705 if (expected != null) { 706 return false; 707 } 708 } else { 709 if (fulltext != null && fulltext.contains(fieldName)) { 710 if (!value.toString().toLowerCase().startsWith(expected.toString().toLowerCase())) { 711 return false; 712 } 713 } else { 714 if (!value.equals(expected)) { 715 return false; 716 } 717 } 718 } 719 } 720 return true; 721 } 722 723 protected boolean isAnonymousMatching(QueryBuilder queryBuilder, Directory dir) { 724 String anonymousUserId = getAnonymousUserId(); 725 if (anonymousUserId == null) { 726 return false; 727 } 728 MultiExpression expression = queryBuilder.predicate(); 729 if (expression.predicates.isEmpty()) { 730 return true; 731 } 732 @SuppressWarnings({ "rawtypes", "unchecked" }) 733 Map<String, Object> entry = (Map) anonymousUser.getProperties(); 734 entry.put(userIdField, anonymousUserId); 735 return new MemoryDirectoryExpressionEvaluator(dir).matchesEntry(expression, entry); 736 } 737 738 @Override 739 public List<NuxeoPrincipal> searchPrincipals(String pattern) { 740 DocumentModelList entries = searchUsers(pattern); 741 List<NuxeoPrincipal> principals = new ArrayList<>(entries.size()); 742 for (DocumentModel entry : entries) { 743 principals.add(makePrincipal(entry)); 744 } 745 return principals; 746 } 747 748 @Override 749 public DocumentModelList searchGroups(String pattern) { 750 return searchGroups(pattern, null); 751 } 752 753 @Override 754 public String getUserSortField() { 755 return userSortField; 756 } 757 758 protected Map<String, String> getUserSortMap() { 759 return getDirectorySortMap(userSortField, userIdField); 760 } 761 762 protected OrderByExpr getUserOrderBy() { 763 String sortField = StringUtils.defaultString(userSortField, userIdField); 764 return OrderByExprs.asc(sortField); 765 } 766 767 protected Map<String, String> getGroupSortMap() { 768 return getDirectorySortMap(groupSortField, groupIdField); 769 } 770 771 protected OrderByExpr getGroupOrderBy() { 772 String sortField = StringUtils.defaultString(groupSortField, groupIdField); 773 return OrderByExprs.asc(sortField); 774 } 775 776 protected Map<String, String> getDirectorySortMap(String descriptorSortField, String fallBackField) { 777 String sortField = descriptorSortField != null ? descriptorSortField : fallBackField; 778 Map<String, String> orderBy = new HashMap<>(); 779 orderBy.put(sortField, DocumentModelComparator.ORDER_ASC); 780 return orderBy; 781 } 782 783 /** 784 * @since 8.2 785 */ 786 protected void notifyCore(String userOrGroupId, String eventId) { 787 notifyCore(userOrGroupId, eventId, null); 788 } 789 790 /** 791 * @since 9.2 792 */ 793 protected void notifyCore(String userOrGroupId, String eventId, List<String> ancestorGroupIds) { 794 Map<String, Serializable> eventProperties = new HashMap<>(); 795 eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, USER_GROUP_CATEGORY); 796 eventProperties.put(ID_PROPERTY_KEY, userOrGroupId); 797 if (ancestorGroupIds != null) { 798 eventProperties.put(ANCESTOR_GROUPS_PROPERTY_KEY, (Serializable) ancestorGroupIds); 799 } 800 NuxeoPrincipal principal = ClientLoginModule.getCurrentPrincipal(); 801 UnboundEventContext envContext = new UnboundEventContext(principal, eventProperties); 802 envContext.setProperties(eventProperties); 803 EventProducer eventProducer = Framework.getService(EventProducer.class); 804 eventProducer.fireEvent(envContext.newEvent(eventId)); 805 } 806 807 protected void notifyRuntime(String userOrGroupName, String eventId) { 808 EventService eventService = Framework.getService(EventService.class); 809 eventService.sendEvent(new Event(USERMANAGER_TOPIC, eventId, this, userOrGroupName)); 810 } 811 812 @Override 813 public void notifyUserChanged(String userName, String eventId) { 814 invalidatePrincipal(userName); 815 notifyRuntime(userName, USERCHANGED_EVENT_ID); 816 if (eventId != null) { 817 notifyRuntime(userName, eventId); 818 notifyCore(userName, eventId); 819 } 820 } 821 822 protected void invalidatePrincipal(String userName) { 823 if (useCache()) { 824 principalCache.invalidate(userName); 825 } 826 } 827 828 @Override 829 public void notifyGroupChanged(String groupName, String eventId, List<String> ancestorGroupNames) { 830 invalidateAllPrincipals(); 831 notifyRuntime(groupName, GROUPCHANGED_EVENT_ID); 832 if (eventId != null) { 833 notifyRuntime(groupName, eventId); 834 notifyCore(groupName, eventId, ancestorGroupNames); 835 } 836 } 837 838 protected void invalidateAllPrincipals() { 839 if (useCache()) { 840 principalCache.invalidateAll(); 841 } 842 } 843 844 @Override 845 public Boolean areGroupsReadOnly() { 846 try (Session groupDir = dirService.open(groupDirectoryName)) { 847 return groupDir.isReadOnly(); 848 } catch (DirectoryException e) { 849 log.error(e); 850 return false; 851 } 852 } 853 854 @Override 855 public Boolean areUsersReadOnly() { 856 try (Session userDir = dirService.open(userDirectoryName)) { 857 return userDir.isReadOnly(); 858 } catch (DirectoryException e) { 859 log.error(e); 860 return false; 861 } 862 } 863 864 protected void checkGrouId(DocumentModel groupModel) { 865 // be sure the name does not contains trailing spaces 866 Object groupIdValue = groupModel.getProperty(groupSchemaName, groupIdField); 867 if (groupIdValue != null) { 868 groupModel.setProperty(groupSchemaName, groupIdField, groupIdValue.toString().trim()); 869 } 870 } 871 872 protected String getGroupId(DocumentModel groupModel) { 873 Object groupIdValue = groupModel.getProperty(groupSchemaName, groupIdField); 874 if (groupIdValue != null && !(groupIdValue instanceof String)) { 875 throw new NuxeoException("Invalid group id " + groupIdValue); 876 } 877 return (String) groupIdValue; 878 } 879 880 protected void checkUserId(DocumentModel userModel) { 881 Object userIdValue = userModel.getProperty(userSchemaName, userIdField); 882 if (userIdValue != null) { 883 userModel.setProperty(userSchemaName, userIdField, userIdValue.toString().trim()); 884 } 885 } 886 887 protected String getUserId(DocumentModel userModel) { 888 Object userIdValue = userModel.getProperty(userSchemaName, userIdField); 889 if (userIdValue != null && !(userIdValue instanceof String)) { 890 throw new NuxeoException("Invalid user id " + userIdValue); 891 } 892 return (String) userIdValue; 893 } 894 895 @Override 896 public DocumentModel createGroup(DocumentModel groupModel) { 897 return createGroup(groupModel, null); 898 } 899 900 @Override 901 public DocumentModel createUser(DocumentModel userModel) { 902 return createUser(userModel, null); 903 } 904 905 @Override 906 public void deleteGroup(String groupId) { 907 deleteGroup(groupId, null); 908 } 909 910 @Override 911 public void deleteGroup(DocumentModel groupModel) { 912 deleteGroup(groupModel, null); 913 } 914 915 @Override 916 public void deleteUser(String userId) { 917 deleteUser(userId, null); 918 } 919 920 @Override 921 public void deleteUser(DocumentModel userModel) { 922 String userId = getUserId(userModel); 923 deleteUser(userId); 924 } 925 926 @Override 927 public List<String> getGroupIds() { 928 try (Session groupDir = dirService.open(groupDirectoryName)) { 929 List<String> groupIds = groupDir.getProjection(Collections.<String, Serializable> emptyMap(), 930 groupDir.getIdField()); 931 Collections.sort(groupIds); 932 return groupIds; 933 } 934 } 935 936 @Override 937 public List<String> getUserIds() { 938 return getUserIds(null); 939 } 940 941 protected void removeVirtualFilters(Map<String, Serializable> filter) { 942 if (filter == null) { 943 return; 944 } 945 List<String> keys = new ArrayList<>(filter.keySet()); 946 for (String key : keys) { 947 if (key.startsWith(VIRTUAL_FIELD_FILTER_PREFIX)) { 948 filter.remove(key); 949 } 950 } 951 } 952 953 protected QueryBuilder getQueryForPattern(String pattern, String dirName, Map<String, MatchType> searchFields, 954 OrderByExpr orderBy) { 955 QueryBuilder queryBuilder = new QueryBuilder(); 956 if (!StringUtils.isBlank(pattern)) { 957 // build query 958 pattern = pattern.trim().toLowerCase(); 959 List<Predicate> predicates = new ArrayList<>(); 960 for (Entry<String, MatchType> fieldEntry : searchFields.entrySet()) { 961 String key = fieldEntry.getKey(); 962 Predicate predicate; 963 if (fieldEntry.getValue() == MatchType.SUBSTRING) { 964 Directory dir = dirService.getDirectory(dirName); 965 SubstringMatchType substringMatchType = dir.getDescriptor().getSubstringMatchType(); 966 String value; 967 switch (substringMatchType) { 968 case subany: 969 value = '%' + pattern + '%'; 970 break; 971 case subinitial: 972 value = pattern + '%'; 973 break; 974 case subfinal: 975 value = '%' + pattern; 976 break; 977 default: 978 throw new IllegalStateException(substringMatchType.toString()); 979 } 980 predicate = Predicates.ilike(key, value); 981 } else { // MatchType.EXACT 982 predicate = Predicates.eq(key, pattern); 983 } 984 predicates.add(predicate); 985 } 986 queryBuilder.filter(new MultiExpression(Operator.OR, predicates)); 987 } 988 queryBuilder.order(orderBy); 989 return queryBuilder; 990 } 991 992 @Override 993 public DocumentModelList searchGroups(Map<String, Serializable> filter, Set<String> fulltext) { 994 return searchGroups(filter, fulltext, null); 995 } 996 997 @Override 998 public DocumentModelList searchGroups(QueryBuilder queryBuilder) { 999 return searchGroups(queryBuilder, null); 1000 } 1001 1002 @Override 1003 public DocumentModelList searchUsers(String pattern) { 1004 return searchUsers(pattern, null); 1005 } 1006 1007 @Override 1008 public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext) { 1009 return searchUsers(filter, fulltext, getUserSortMap(), null); 1010 } 1011 1012 @Override 1013 public DocumentModelList searchUsers(QueryBuilder queryBuilder) { 1014 return searchUsers(queryBuilder, null); 1015 } 1016 1017 @Override 1018 public void updateGroup(DocumentModel groupModel) { 1019 updateGroup(groupModel, null); 1020 } 1021 1022 @Override 1023 public void updateUser(DocumentModel userModel) { 1024 updateUser(userModel, null); 1025 } 1026 1027 @Override 1028 public DocumentModel getBareGroupModel() { 1029 String schema = dirService.getDirectorySchema(groupDirectoryName); 1030 return BaseSession.createEntryModel(null, schema, null, null); 1031 } 1032 1033 @Override 1034 public List<String> getAdministratorsGroups() { 1035 return administratorGroups; 1036 } 1037 1038 protected List<String> getLeafPermissions(String perm) { 1039 ArrayList<String> permissions = new ArrayList<>(); 1040 PermissionProvider permissionProvider = Framework.getService(PermissionProvider.class); 1041 String[] subpermissions = permissionProvider.getSubPermissions(perm); 1042 if (subpermissions == null || subpermissions.length <= 0) { 1043 // it's a leaf 1044 permissions.add(perm); 1045 return permissions; 1046 } 1047 for (String subperm : subpermissions) { 1048 permissions.addAll(getLeafPermissions(subperm)); 1049 } 1050 return permissions; 1051 } 1052 1053 @Override 1054 public String[] getUsersForPermission(String perm, ACP acp) { 1055 return getUsersForPermission(perm, acp, null); 1056 } 1057 1058 @Override 1059 public Principal authenticate(String name, String password) { 1060 return checkUsernamePassword(name, password) ? getPrincipal(name) : null; 1061 } 1062 1063 /*************** MULTI-TENANT-IMPLEMENTATION ************************/ 1064 1065 public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext, 1066 Map<String, String> orderBy, DocumentModel context) { 1067 try (Session userDir = dirService.open(userDirectoryName, context)) { 1068 removeVirtualFilters(filter); 1069 1070 // XXX: do not fetch references, can be costly 1071 DocumentModelList entries = userDir.query(filter, fulltext, null, false); 1072 if (isAnonymousMatching(filter, fulltext)) { 1073 entries.add(makeVirtualUserEntry(getAnonymousUserId(), anonymousUser)); 1074 } 1075 1076 // TODO: match searchable virtual users 1077 1078 if (orderBy != null && !orderBy.isEmpty()) { 1079 // sort: cannot sort before virtual users are added 1080 entries.sort(new DocumentModelComparator(userSchemaName, orderBy)); 1081 } 1082 1083 return entries; 1084 } 1085 } 1086 1087 @Override 1088 public List<String> getUsersInGroup(String groupId, DocumentModel context) { 1089 String storeGroupId = multiTenantManagement.groupnameTranformer(this, groupId, context); 1090 return getGroup(storeGroupId).getMemberUsers(); 1091 } 1092 1093 @Override 1094 public DocumentModelList searchUsers(String pattern, DocumentModel context) { 1095 QueryBuilder queryBuilder = getQueryForPattern(pattern, userDirectoryName, userSearchFields, getUserOrderBy()); 1096 return searchUsers(queryBuilder, context); 1097 } 1098 1099 @Override 1100 public DocumentModelList searchUsers(QueryBuilder queryBuilder, DocumentModel context) { 1101 Directory dir = dirService.getDirectory(userDirectoryName, context); 1102 try (Session session = dir.getSession()) { 1103 if (isAnonymousMatching(queryBuilder, dir)) { 1104 DocumentModel anonymousEntry = makeVirtualUserEntry(getAnonymousUserId(), anonymousUser); 1105 List<DocumentModel> virtualEntries = Collections.singletonList(anonymousEntry); 1106 return queryWithVirtualEntries(session, queryBuilder, virtualEntries); 1107 } else { 1108 return session.query(queryBuilder, false); 1109 } 1110 } 1111 } 1112 1113 /** 1114 * Executes a query then adds virtual entries (already supposed to match the query). Then does 1115 * limit/offset/order/countTotal. 1116 * 1117 * @since 10.3 1118 */ 1119 protected DocumentModelList queryWithVirtualEntries(Session session, QueryBuilder queryBuilder, 1120 List<DocumentModel> virtualEntries) { 1121 AbstractDirectory dir = (AbstractDirectory) ((BaseSession) session).getDirectory(); 1122 1123 // do the basic query 1124 DocumentModelList entries = session.query(queryBuilder, false); 1125 1126 int limit = Math.max(0, (int) queryBuilder.limit()); 1127 int offset = Math.max(0, (int) queryBuilder.offset()); 1128 boolean countTotal = queryBuilder.countTotal(); 1129 OrderByList orders = queryBuilder.orders(); 1130 int size = entries.size(); 1131 long totalSize = entries.totalSize(); 1132 1133 // if we have all the results or a page small enough that all virtual entries fit, just add them 1134 if (offset == 0 && (limit == 0 || size + virtualEntries.size() <= limit)) { 1135 // add virtual entries 1136 entries.addAll(virtualEntries); 1137 if (totalSize >= 0) { 1138 ((DocumentModelListImpl) entries).setTotalSize(totalSize + virtualEntries.size()); 1139 } 1140 // re-sort 1141 dir.orderEntries(entries, AbstractDirectory.makeOrderBy(orders)); 1142 return entries; 1143 } 1144 1145 // else we have to do a manual paging/sorting... 1146 // re-do the full query with limit/offset 1147 queryBuilder = new QueryBuilder(queryBuilder).limit(0).offset(0).orders(Collections.emptyList()); 1148 entries = session.query(queryBuilder, false); 1149 // add virtual entries 1150 entries.addAll(virtualEntries); 1151 // sort 1152 if (!orders.isEmpty()) { 1153 dir.orderEntries(entries, AbstractDirectory.makeOrderBy(orders)); 1154 } 1155 // manual limit/offset 1156 entries = ((BaseSession) session).applyQueryLimits(entries, limit, offset); 1157 if (!countTotal) { // offset != 0 always here 1158 // compat with other directories 1159 ((DocumentModelListImpl) entries).setTotalSize(-2); 1160 } 1161 return entries; 1162 } 1163 1164 @Override 1165 public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext, 1166 DocumentModel context) { 1167 throw new UnsupportedOperationException(); 1168 } 1169 1170 @Override 1171 public List<String> getGroupIds(DocumentModel context) { 1172 throw new UnsupportedOperationException(); 1173 } 1174 1175 @Override 1176 public DocumentModelList searchGroups(Map<String, Serializable> filter, Set<String> fulltext, 1177 DocumentModel context) { 1178 filter = filter != null ? cloneMap(filter) : new HashMap<>(); 1179 HashSet<String> fulltextClone = fulltext != null ? cloneSet(fulltext) : new HashSet<>(); 1180 multiTenantManagement.queryTransformer(this, filter, fulltextClone, context); 1181 1182 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1183 removeVirtualFilters(filter); 1184 return groupDir.query(filter, fulltextClone, getGroupSortMap(), false); 1185 } 1186 } 1187 1188 @Override 1189 public DocumentModelList searchGroups(QueryBuilder queryBuilder, DocumentModel context) { 1190 queryBuilder = multiTenantManagement.groupQueryTransformer(this, queryBuilder, context); 1191 try (Session session = dirService.open(groupDirectoryName, context)) { 1192 return session.query(queryBuilder, false); 1193 } 1194 } 1195 1196 @Override 1197 public DocumentModel createGroup(DocumentModel groupModel, DocumentModel context) 1198 throws GroupAlreadyExistsException { 1199 groupModel = multiTenantManagement.groupTransformer(this, groupModel, context); 1200 1201 // be sure the name does not contains trailing spaces 1202 checkGrouId(groupModel); 1203 1204 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1205 String groupId = getGroupId(groupModel); 1206 1207 // check the group does not exist 1208 if (groupDir.hasEntry(groupId)) { 1209 throw new GroupAlreadyExistsException(); 1210 } 1211 groupModel = groupDir.createEntry(groupModel); 1212 notifyGroupChanged(groupId, GROUPCREATED_EVENT_ID); 1213 return groupModel; 1214 1215 } 1216 } 1217 1218 @Override 1219 public DocumentModel getGroupModel(String groupIdValue, DocumentModel context) { 1220 String groupName = multiTenantManagement.groupnameTranformer(this, groupIdValue, context); 1221 if (groupName != null) { 1222 groupName = groupName.trim(); 1223 } 1224 1225 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1226 return groupDir.getEntry(groupName); 1227 } 1228 } 1229 1230 @Override 1231 public DocumentModel getUserModel(String userName, DocumentModel context) { 1232 if (userName == null) { 1233 return null; 1234 } 1235 1236 userName = userName.trim(); 1237 // return anonymous model 1238 if (anonymousUser != null && userName.equals(anonymousUser.getId())) { 1239 return makeVirtualUserEntry(getAnonymousUserId(), anonymousUser); 1240 } 1241 1242 try (Session userDir = dirService.open(userDirectoryName, context)) { 1243 return userDir.getEntry(userName); 1244 } 1245 } 1246 1247 protected Map<String, Serializable> cloneMap(Map<String, Serializable> map) { 1248 Map<String, Serializable> result = new HashMap<>(); 1249 for (String key : map.keySet()) { 1250 result.put(key, map.get(key)); 1251 } 1252 return result; 1253 } 1254 1255 protected HashSet<String> cloneSet(Set<String> set) { 1256 HashSet<String> result = new HashSet<>(); 1257 for (String key : set) { 1258 result.add(key); 1259 } 1260 return result; 1261 } 1262 1263 @Override 1264 public NuxeoPrincipal getPrincipal(String username, DocumentModel context) { 1265 if (username == null) { 1266 return null; 1267 } 1268 String anonymousUserId = getAnonymousUserId(); 1269 if (username.equals(anonymousUserId)) { 1270 return makeAnonymousPrincipal(); 1271 } 1272 if (virtualUsers.containsKey(username)) { 1273 return makeVirtualPrincipal(virtualUsers.get(username)); 1274 } 1275 if (NuxeoPrincipal.isTransientUsername(username)) { 1276 return makeTransientPrincipal(username); 1277 } 1278 DocumentModel userModel = getUserModel(username, context); 1279 if (userModel != null) { 1280 return makePrincipal(userModel); 1281 } 1282 return null; 1283 } 1284 1285 @Override 1286 public DocumentModelList searchGroups(String pattern, DocumentModel context) { 1287 QueryBuilder queryBuilder = getQueryForPattern(pattern, groupDirectoryName, groupSearchFields, 1288 getGroupOrderBy()); 1289 return searchGroups(queryBuilder, context); 1290 } 1291 1292 @Override 1293 public List<String> getUserIds(DocumentModel context) { 1294 try (Session userDir = dirService.open(userDirectoryName, context)) { 1295 List<String> userIds = userDir.getProjection(Collections.<String, Serializable> emptyMap(), 1296 userDir.getIdField()); 1297 Collections.sort(userIds); 1298 return userIds; 1299 } 1300 } 1301 1302 @Override 1303 public DocumentModel createUser(DocumentModel userModel, DocumentModel context) throws UserAlreadyExistsException { 1304 // be sure UserId does not contains any trailing spaces 1305 checkUserId(userModel); 1306 1307 try (Session userDir = dirService.open(userDirectoryName, context)) { 1308 String userId = getUserId(userModel); 1309 1310 // check the user does not exist 1311 if (userDir.hasEntry(userId)) { 1312 throw new UserAlreadyExistsException(); 1313 } 1314 1315 checkPasswordValidity(userModel); 1316 1317 String schema = dirService.getDirectorySchema(userDirectoryName); 1318 // If trying to create the user with groups, check that the groups exist 1319 checkGroupsExistence(userModel, schema, context); 1320 1321 String clearUsername = (String) userModel.getProperty(schema, userDir.getIdField()); 1322 String clearPassword = (String) userModel.getProperty(schema, userDir.getPasswordField()); 1323 1324 userModel = userDir.createEntry(userModel); 1325 1326 syncDigestAuthPassword(clearUsername, clearPassword); 1327 1328 notifyUserChanged(userId, USERCREATED_EVENT_ID); 1329 return userModel; 1330 1331 } 1332 } 1333 1334 @SuppressWarnings("unchecked") 1335 protected void checkGroupsExistence(DocumentModel userModel, String schema, DocumentModel context) { 1336 List<String> groups = (List<String>) userModel.getProperty(schema, GROUPS_COLUMN); 1337 if (groups == null || groups.isEmpty()) { 1338 return; 1339 } 1340 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1341 for (String group : groups) { 1342 if (!groupDir.hasEntry(group)) { 1343 throw new NuxeoException("group does not exist: " + group, SC_FORBIDDEN); 1344 } 1345 } 1346 } 1347 } 1348 1349 protected void checkPasswordValidity(DocumentModel userModel) throws InvalidPasswordException { 1350 if (!mustCheckPasswordValidity()) { 1351 return; 1352 } 1353 String schema = dirService.getDirectorySchema(userDirectoryName); 1354 String passwordField = dirService.getDirectory(userDirectoryName).getPasswordField(); 1355 1356 Property passwordProperty = userModel.getPropertyObject(schema, passwordField); 1357 1358 if (passwordProperty.isDirty()) { 1359 String clearPassword = (String) passwordProperty.getValue(); 1360 if (StringUtils.isNotBlank(clearPassword) && !validatePassword(clearPassword)) { 1361 throw new InvalidPasswordException(); 1362 } 1363 } 1364 } 1365 1366 @Override 1367 public void updateUser(DocumentModel userModel, DocumentModel context) { 1368 try (Session userDir = dirService.open(userDirectoryName, context)) { 1369 String userId = getUserId(userModel); 1370 1371 if (!userDir.hasEntry(userId)) { 1372 throw new DirectoryException("user does not exist: " + userId); 1373 } 1374 1375 String schema = dirService.getDirectorySchema(userDirectoryName); 1376 // If trying to update the user with groups, check that the groups exist 1377 checkGroupsExistence(userModel, schema, context); 1378 1379 checkPasswordValidity(userModel); 1380 1381 String clearUsername = (String) userModel.getProperty(schema, userDir.getIdField()); 1382 String clearPassword = (String) userModel.getProperty(schema, userDir.getPasswordField()); 1383 1384 userDir.updateEntry(userModel); 1385 1386 syncDigestAuthPassword(clearUsername, clearPassword); 1387 1388 notifyUserChanged(userId, USERMODIFIED_EVENT_ID); 1389 } 1390 } 1391 1392 private boolean mustCheckPasswordValidity() { 1393 return Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(VALIDATE_PASSWORD_PARAM); 1394 } 1395 1396 @Override 1397 public void deleteUser(DocumentModel userModel, DocumentModel context) { 1398 String userId = getUserId(userModel); 1399 deleteUser(userId, context); 1400 } 1401 1402 @Override 1403 public void deleteUser(String userId, DocumentModel context) { 1404 try (Session userDir = dirService.open(userDirectoryName, context)) { 1405 if (!userDir.hasEntry(userId)) { 1406 throw new DirectoryException("User does not exist: " + userId); 1407 } 1408 userDir.deleteEntry(userId); 1409 notifyUserChanged(userId, USERDELETED_EVENT_ID); 1410 1411 } finally { 1412 notifyUserChanged(userId, null); 1413 } 1414 } 1415 1416 @Override 1417 public void updateGroup(DocumentModel groupModel, DocumentModel context) { 1418 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1419 String groupId = getGroupId(groupModel); 1420 1421 if (!groupDir.hasEntry(groupId)) { 1422 throw new DirectoryException("group does not exist: " + groupId); 1423 } 1424 groupDir.updateEntry(groupModel); 1425 notifyGroupChanged(groupId, GROUPMODIFIED_EVENT_ID); 1426 } 1427 } 1428 1429 @Override 1430 public void deleteGroup(DocumentModel groupModel, DocumentModel context) { 1431 String groupId = getGroupId(groupModel); 1432 deleteGroup(groupId, context); 1433 } 1434 1435 @Override 1436 public void deleteGroup(String groupId, DocumentModel context) { 1437 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1438 if (!groupDir.hasEntry(groupId)) { 1439 throw new DirectoryException("Group does not exist: " + groupId); 1440 } 1441 // Get ancestor group names before deletion to pass them as a property of the core event 1442 List<String> ancestorGroupNames = getAncestorGroups(groupId); 1443 groupDir.deleteEntry(groupId); 1444 notifyGroupChanged(groupId, GROUPDELETED_EVENT_ID, ancestorGroupNames); 1445 } 1446 } 1447 1448 @Override 1449 public List<String> getGroupsInGroup(String parentId, DocumentModel context) { 1450 return getGroup(parentId, null).getMemberGroups(); 1451 } 1452 1453 @Override 1454 public List<String> getTopLevelGroups(DocumentModel context) { 1455 try (Session groupDir = dirService.open(groupDirectoryName, context)) { 1456 List<String> topLevelGroups = new LinkedList<>(); 1457 // XXX retrieve all entries with references, can be costly. 1458 DocumentModelList groups = groupDir.query(Collections.<String, Serializable> emptyMap(), null, null, true); 1459 for (DocumentModel group : groups) { 1460 @SuppressWarnings("unchecked") 1461 List<String> parents = (List<String>) group.getProperty(groupSchemaName, groupParentGroupsField); 1462 1463 if (parents == null || parents.isEmpty()) { 1464 topLevelGroups.add(group.getId()); 1465 } 1466 } 1467 return topLevelGroups; 1468 } 1469 } 1470 1471 @Override 1472 public List<String> getUsersInGroupAndSubGroups(String groupId, DocumentModel context) { 1473 Set<String> groups = new HashSet<>(); 1474 groups.add(groupId); 1475 appendSubgroups(groupId, groups, context); 1476 1477 Set<String> users = new HashSet<>(); 1478 for (String groupid : groups) { 1479 users.addAll(getGroup(groupid, context).getMemberUsers()); 1480 } 1481 1482 return new ArrayList<>(users); 1483 } 1484 1485 @Override 1486 public String[] getUsersForPermission(String perm, ACP acp, DocumentModel context) { 1487 PermissionProvider permissionProvider = Framework.getService(PermissionProvider.class); 1488 // using a hashset to avoid duplicates 1489 HashSet<String> usernames = new HashSet<>(); 1490 1491 ACL merged = acp.getMergedACLs("merged"); 1492 // The list of permission that is has "perm" as its (compound) 1493 // permission 1494 ArrayList<ACE> filteredACEbyPerm = new ArrayList<>(); 1495 1496 List<String> currentPermissions = getLeafPermissions(perm); 1497 1498 for (ACE ace : merged.getACEs()) { 1499 // Checking if the permission contains the permission we want to 1500 // check (we use the security service method for coumpound 1501 // permissions) 1502 List<String> acePermissions = getLeafPermissions(ace.getPermission()); 1503 1504 // Everything is a special permission (not compound) 1505 if (SecurityConstants.EVERYTHING.equals(ace.getPermission())) { 1506 acePermissions = Arrays.asList(permissionProvider.getPermissions()); 1507 } 1508 1509 if (acePermissions.containsAll(currentPermissions)) { 1510 // special case: everybody perm grant false, don't take in 1511 // account the previous ace 1512 if (SecurityConstants.EVERYONE.equals(ace.getUsername()) && !ace.isGranted()) { 1513 break; 1514 } 1515 filteredACEbyPerm.add(ace); 1516 } 1517 } 1518 1519 for (ACE ace : filteredACEbyPerm) { 1520 String aceUsername = ace.getUsername(); 1521 List<String> users = null; 1522 // If everyone, add/remove all the users 1523 if (SecurityConstants.EVERYONE.equals(aceUsername)) { 1524 users = getUserIds(); 1525 } 1526 // if a group, add/remove all the user from the group (and 1527 // subgroups) 1528 if (users == null) { 1529 NuxeoGroup group; 1530 group = getGroup(aceUsername, context); 1531 if (group != null) { 1532 users = getUsersInGroupAndSubGroups(aceUsername, context); 1533 } 1534 1535 } 1536 // otherwise, add the user 1537 if (users == null) { 1538 users = new ArrayList<>(); 1539 users.add(aceUsername); 1540 } 1541 if (ace.isGranted()) { 1542 usernames.addAll(users); 1543 } else { 1544 usernames.removeAll(users); 1545 } 1546 } 1547 return usernames.toArray(new String[usernames.size()]); 1548 } 1549 1550 @Override 1551 public List<String> getAncestorGroups(String groupId) { 1552 List<String> ancestorGroups = new ArrayList<>(); 1553 populateAncestorGroups(groupId, ancestorGroups); 1554 return ancestorGroups; 1555 } 1556 1557 protected void populateAncestorGroups(String groupId, List<String> ancestorGroups) { 1558 NuxeoGroup group = getGroup(groupId); 1559 if (group != null) { 1560 List<String> parentGroups = group.getParentGroups(); 1561 // Avoid infinite loop in case a group has one of its parents as a subgroup 1562 parentGroups.stream().filter(parentGroup -> !ancestorGroups.contains(parentGroup)).forEach(parentGroup -> { 1563 ancestorGroups.add(parentGroup); 1564 populateAncestorGroups(parentGroup, ancestorGroups); 1565 }); 1566 } 1567 } 1568 1569 @Override 1570 public GroupConfig getGroupConfig() { 1571 return groupConfig; 1572 } 1573 1574 @Override 1575 public void handleEvent(Event event) { 1576 String id = event.getId(); 1577 if (INVALIDATE_PRINCIPAL_EVENT_ID.equals(id)) { 1578 invalidatePrincipal((String) event.getData()); 1579 } else if (INVALIDATE_ALL_PRINCIPALS_EVENT_ID.equals(id)) { 1580 invalidateAllPrincipals(); 1581 } 1582 } 1583 1584}