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