001/* 002 * Copyright 2020 Anyware Services 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 */ 016package org.ametys.plugins.repository.jcr; 017 018import java.util.Collections; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.Map; 022import java.util.Set; 023import java.util.stream.Collectors; 024 025import javax.jcr.AccessDeniedException; 026import javax.jcr.ItemNotFoundException; 027import javax.jcr.Node; 028import javax.jcr.NodeIterator; 029import javax.jcr.PathNotFoundException; 030import javax.jcr.Repository; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033import javax.jcr.Value; 034import javax.jcr.lock.Lock; 035import javax.jcr.lock.LockManager; 036import javax.jcr.query.Query; 037 038import org.apache.avalon.framework.component.Component; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.avalon.framework.service.Serviceable; 042import org.apache.jackrabbit.JcrConstants; 043import org.apache.jackrabbit.util.ISO9075; 044import org.apache.jackrabbit.util.Text; 045import org.slf4j.Logger; 046 047import org.ametys.core.group.GroupIdentity; 048import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys; 049import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup; 050import org.ametys.core.user.UserIdentity; 051import org.ametys.core.util.LambdaUtils; 052import org.ametys.plugins.repository.ACLAmetysObject; 053import org.ametys.plugins.repository.AmetysObject; 054import org.ametys.plugins.repository.AmetysObjectResolver; 055import org.ametys.plugins.repository.AmetysRepositoryException; 056import org.ametys.plugins.repository.ModifiableACLAmetysObject; 057import org.ametys.plugins.repository.ModifiableACLAmetysObjectProfileAssignmentStorage; 058import org.ametys.plugins.repository.RepositoryConstants; 059import org.ametys.plugins.repository.provider.AbstractRepository; 060import org.ametys.plugins.repository.query.expression.Expression; 061import org.ametys.plugins.repository.query.expression.Expression.LogicalOperator; 062import org.ametys.plugins.repository.query.expression.OrExpression; 063import org.ametys.runtime.plugin.component.LogEnabled; 064 065/** 066 * Helper for implementing {@link ModifiableACLAmetysObject} in JCR under its node. 067 */ 068public class ACLJCRAmetysObjectHelper implements Component, Serviceable, LogEnabled 069{ 070 /** The AmetysObject resolver */ 071 protected static AmetysObjectResolver _resolver; 072 /** The repository */ 073 protected static Repository _repository; 074 075 private static final String __NODE_NAME_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":acl"; 076 private static final String __NODETYPE_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX + ":acl"; 077 078 private static final String __NODE_NAME_ACL_USERS = "users"; 079 private static final String __NODE_NAME_ACL_GROUPS = "groups"; 080 private static final String __NODETYPE_ACL_USER = RepositoryConstants.NAMESPACE_PREFIX + ":acl-user"; 081 private static final String __NODETYPE_ACL_GROUP = RepositoryConstants.NAMESPACE_PREFIX + ":acl-group"; 082 083 private static final String __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-any-connected-profiles"; 084 private static final String __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-any-connected-profiles"; 085 private static final String __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-anonymous-profiles"; 086 private static final String __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-anonymous-profiles"; 087 088 private static final String __PROPERTY_NAME_ALLOWED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles"; 089 private static final String __PROPERTY_NAME_DENIED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-profiles"; 090 091 private static final String __PROPERTY_NAME_DISALLOW_INHERITANCE = RepositoryConstants.NAMESPACE_PREFIX + ":disallow-inheritance"; 092 093 private static final Map<AnonymousOrAnyConnectedKeys, Set<String>> __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT = Map.of( 094 AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, Set.of(), 095 AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, Set.of(), 096 AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, Set.of(), 097 AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, Set.of()); 098 private static final Map<UserOrGroup, Set<String>> __USER_OR_GROUP_NORIGHT = Map.of( 099 UserOrGroup.ALLOWED, Set.of(), 100 UserOrGroup.DENIED, Set.of()); 101 102 103 private static Logger _logger; 104 105 @Override 106 public void service(ServiceManager manager) throws ServiceException 107 { 108 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 109 _repository = (Repository) manager.lookup(AbstractRepository.ROLE); 110 } 111 112 public void setLogger(Logger logger) 113 { 114 _logger = logger; 115 } 116 117 118 /* -------------- */ 119 /* HAS PERMISSION */ 120 /* -------------- */ 121 122 private static Set<String> _convertNodeToPath(Set<? extends Object> rootNodes) 123 { 124 return rootNodes.stream().filter(JCRAmetysObject.class::isInstance).map(JCRAmetysObject.class::cast).map(LambdaUtils.wrap(ao -> ISO9075.encodePath(ao.getNode().getPath()))).collect(Collectors.toSet()); 125 } 126 127 128 /** 129 * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for the user 130 * @param user The user 131 * @param profileIds The ids of the profiles to check 132 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search. 133 * @return If the Set is empty, it means the user has no matching profile.<br> 134 * If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for the user AND it can contains some other profiles that were not in the given profiles 135 */ 136 public static Set<String> hasUserAnyAllowedProfile(Set<? extends Object> rootNodes, UserIdentity user, Set<String> profileIds) 137 { 138 Expression expr = new AllowedProfileExpression(profileIds.toArray(new String[profileIds.size()])); 139 for (String rootPath : _convertNodeToPath(rootNodes)) 140 { 141 NodeIterator nodes = getACLUsers(user, rootPath, expr); 142 143 if (nodes.hasNext()) 144 { 145 // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can 146 Node userNode = nodes.nextNode(); 147 return _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES); 148 } 149 } 150 return Set.of(); 151 } 152 153 /** 154 * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for the group 155 * @param groups The groups 156 * @param profileIds The ids of the profiles 157 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search. 158 * @return If the Set is empty, it means the group has no matching profile.<br> 159 * If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for the group AND it can contains some other profiles that were not in the given profiles 160 */ 161 public static Set<String> hasGroupAnyAllowedProfile(Set<? extends Object> rootNodes, Set<GroupIdentity> groups, Set<String> profileIds) 162 { 163 if (!groups.isEmpty()) 164 { 165 Expression expr = new AllowedProfileExpression(profileIds.toArray(new String[profileIds.size()])); 166 for (String rootPath : _convertNodeToPath(rootNodes)) 167 { 168 // Approximative query (to be fast) 169 NodeIterator nodes = _getApprochingACLGroups(groups, rootPath, expr); 170 171 while (nodes.hasNext()) 172 { 173 Node groupNode = nodes.nextNode(); 174 175 // As the query was a fast approximative request, we now check if the result is fine 176 String groupId; 177 String directoryId; 178 try 179 { 180 groupId = Text.unescapeIllegalJcrChars(groupNode.getName()); 181 directoryId = groupNode.getParent().getName(); 182 } 183 catch (RepositoryException ex) 184 { 185 throw new AmetysRepositoryException("An error occured getting group information", ex); 186 } 187 188 if (groups.contains(new GroupIdentity(groupId, directoryId))) 189 { 190 // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can 191 return _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES); 192 } 193 } 194 } 195 } 196 197 return Set.of(); 198 } 199 200 /** 201 * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for any connected user 202 * @param profileIds The ids of the profiles 203 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search. 204 * @return If the Set is empty, it means any connected user has no matching profile.<br> 205 * If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for anyconnected user AND it can contains some other profiles that were not in the given profiles 206 */ 207 public static Set<String> hasAnyConnectedAnyAllowedProfile(Set<? extends Object> rootNodes, Set<String> profileIds) 208 { 209 Expression expr = new AnyConnectedAllowedProfileExpression(profileIds.toArray(new String[profileIds.size()])); 210 for (String rootPath : _convertNodeToPath(rootNodes)) 211 { 212 NodeIterator nodes = getACLRoots(rootPath, expr); 213 214 if (nodes.hasNext()) 215 { 216 // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can 217 Node aclNode = nodes.nextNode(); 218 return _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES); 219 } 220 } 221 return Set.of(); 222 } 223 224 225 /** 226 * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for anonymous 227 * @param profileIds The ids of the profiles 228 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search. 229 * @return If the Set is empty, it means anonymous has no matching profile.<br> 230 * If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for anonymous AND it can contains some other profiles that were not in the given profiles 231 */ 232 public static Set<String> hasAnonymousAnyAllowedProfile(Set<? extends Object> rootNodes, Set<String> profileIds) 233 { 234 Expression expr = new AnonymousAllowedProfileExpression(profileIds.toArray(new String[profileIds.size()])); 235 for (String rootPath : _convertNodeToPath(rootNodes)) 236 { 237 NodeIterator nodes = getACLRoots(rootPath, expr); 238 239 if (nodes.hasNext()) 240 { 241 // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can 242 Node aclNode = nodes.nextNode(); 243 return _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES); 244 } 245 } 246 return Set.of(); 247 } 248 249 /** 250 * Gets all contexts with stored profiles (allowed or denied) for anonymous or any connected user and for each, a description of the permission 251 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query 252 * @return a map associating a context object to the stored profile for each permission 253 */ 254 public static Map<Object, Map<AnonymousOrAnyConnectedKeys, Set<String>>> getAllProfilesForAnonymousAndAnyConnectedUser(Set< ? extends Object> rootNodes) 255 { 256 Map<Object, Map<AnonymousOrAnyConnectedKeys, Set<String>>> result = new HashMap<>(); 257 // Only retrieve node with assignments to anonymous or any connected 258 Expression predicate = new OrExpression( 259 () -> "@" + __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, 260 () -> "@" + __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, 261 () -> "@" + __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, 262 () -> "@" + __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES 263 ); 264 265 for (String rootPath : _convertNodeToPath(rootNodes)) 266 { 267 NodeIterator nodes = getACLRoots(rootPath, predicate); 268 269 while (nodes.hasNext()) 270 { 271 Node aclNode = nodes.nextNode(); 272 try 273 { 274 Map<AnonymousOrAnyConnectedKeys, Set<String>> aoResult = new HashMap<>(); 275 Set<String> allowedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES); 276 if (!allowedAnonymous.isEmpty()) 277 { 278 aoResult.put(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, allowedAnonymous); 279 } 280 281 Set<String> deniedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES); 282 if (!deniedAnonymous.isEmpty()) 283 { 284 aoResult.put(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, deniedAnonymous); 285 } 286 Set<String> allowedAny = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES); 287 if (!allowedAny.isEmpty()) 288 { 289 aoResult.put(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, allowedAny); 290 } 291 Set<String> deniedAny = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES); 292 if (!deniedAny.isEmpty()) 293 { 294 aoResult.put(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, deniedAny); 295 } 296 297 if (!aoResult.isEmpty()) 298 { 299 AmetysObject ao = _getAmetysObjectFromACLNode(aclNode); 300 result.put(ao, aoResult); 301 } 302 } 303 catch (RepositoryException e) 304 { 305 _logger.error("Failed to retrieve object for acl node " + aclNode.toString() + ". The node will be ignored."); 306 } 307 } 308 } 309 return result; 310 } 311 312 /** 313 * Gets all context with stored profiles (allowed or denied) for the groups and for each, a description of the permission 314 * Gets the groups that have allowed profiles assigned on the given object 315 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query 316 * @param groups The groups to get profiles for. 317 * @return The map of context with their assigned permissions 318 */ 319 public static Map<Object, Map<GroupIdentity, Map<UserOrGroup, Set<String>>>> getAllProfilesForGroups(Set< ? extends Object> rootNodes, Set<GroupIdentity> groups) 320 { 321 Map<Object, Map<GroupIdentity, Map<UserOrGroup, Set<String>>>> result = new HashMap<>(); 322 if (!groups.isEmpty()) 323 { 324 for (String rootPath : _convertNodeToPath(rootNodes)) 325 { 326 // Approximative query (to be fast) 327 NodeIterator nodes = _getApprochingACLGroups(groups, rootPath, null); 328 329 while (nodes.hasNext()) 330 { 331 Node groupNode = nodes.nextNode(); 332 333 // As the query was a fast approximative request, we now check if the result is fine 334 String groupId; 335 String directoryId; 336 try 337 { 338 groupId = Text.unescapeIllegalJcrChars(groupNode.getName()); 339 directoryId = groupNode.getParent().getName(); 340 } 341 catch (RepositoryException ex) 342 { 343 throw new AmetysRepositoryException("An error occured getting group information", ex); 344 } 345 346 GroupIdentity currentGroup = new GroupIdentity(groupId, directoryId); 347 if (groups.contains(currentGroup)) 348 { 349 try 350 { 351 352 // Determine the group permissions 353 Map<UserOrGroup, Set<String>> groupPermissions = new HashMap<>(); 354 355 Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES); 356 if (!allowedProfiles.isEmpty()) 357 { 358 groupPermissions.put(UserOrGroup.ALLOWED, allowedProfiles); 359 } 360 Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES); 361 if (!deniedProfiles.isEmpty()) 362 { 363 groupPermissions.put(UserOrGroup.DENIED, deniedProfiles); 364 } 365 366 // Only add actual permissions to the result 367 if (!groupPermissions.isEmpty()) 368 { 369 AmetysObject ao = _getAmetysObjectFromACLNode(groupNode); 370 // The ametys object could already be in the result map having permissions from an other group 371 Map<GroupIdentity, Map<UserOrGroup, Set<String>>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>()); 372 // There can only be one node per group, so we don't need to retrieve existing value 373 objectPermissions.put(currentGroup, groupPermissions); 374 } 375 } 376 catch (RepositoryException e) 377 { 378 _logger.error("Failed to retrieve object for group acl node " + groupNode.toString() + ". The node will be ignored."); 379 } 380 } 381 } 382 } 383 } 384 return result; 385 } 386 387 /** 388 * Gets all context with stored profiles (allowed or denied) for the user and for each, a description of the permission 389 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query 390 * @param user The user to get profiles for. 391 * @return The map of context with their assigned allowed/denied profiles 392 */ 393 public static Map<Object, Map<UserOrGroup, Set<String>>> getAllProfilesForUser(Set< ? extends Object> rootNodes, UserIdentity user) 394 { 395 Map<Object, Map<UserOrGroup, Set<String>>> result = new HashMap<>(); 396 397 for (String rootPath : _convertNodeToPath(rootNodes)) 398 { 399 NodeIterator nodes = getACLUsers(user, rootPath, null); 400 401 while (nodes.hasNext()) 402 { 403 Node userNode = nodes.nextNode(); 404 try 405 { 406 Map<UserOrGroup, Set<String>> aoResult = new HashMap<>(); 407 Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES); 408 if (!allowedProfiles.isEmpty()) 409 { 410 aoResult.put(UserOrGroup.ALLOWED, allowedProfiles); 411 } 412 Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES); 413 if (!deniedProfiles.isEmpty()) 414 { 415 aoResult.put(UserOrGroup.DENIED, deniedProfiles); 416 } 417 418 if (!aoResult.isEmpty()) 419 { 420 AmetysObject ao = _getAmetysObjectFromACLNode(userNode); 421 result.put(ao, aoResult); 422 } 423 } 424 catch (RepositoryException e) 425 { 426 _logger.error("Failed to retrieve object for user acl node " + userNode.toString() + ". The node will be ignored."); 427 } 428 } 429 } 430 return result; 431 } 432 433 private static AmetysObject _getAmetysObjectFromACLNode(Node node) throws RepositoryException, ItemNotFoundException, AccessDeniedException 434 { 435 switch (node.getPrimaryNodeType().getName()) 436 { 437 case __NODETYPE_ROOT_ACL: 438 return _resolver.resolve(node.getParent(), false); 439 case __NODETYPE_ACL_USER: 440 case __NODETYPE_ACL_GROUP: 441 return _resolver.resolve(node.getParent().getParent().getParent().getParent(), false); 442 default: 443 return null; 444 } 445 } 446 447 /** 448 * Returns all ACL root objects (ametys:acl nodes) 449 * @param rootPath The root path to restrict the search. Can be null. 450 * @param predicat The predicat expression. Can be null. 451 * @return The ACL root objects 452 */ 453 public static NodeIterator getACLRoots (String rootPath, Expression predicat) 454 { 455 StringBuilder sb = new StringBuilder("/jcr:root"); 456 457 if (rootPath != null) 458 { 459 sb.append(rootPath); 460 } 461 462 sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")"); 463 464 if (predicat != null) 465 { 466 sb.append("[").append(predicat.build()).append("]"); 467 } 468 469 return _query(sb.toString()); 470 } 471 472 /** 473 * Returns all ACL objects for a given user (ametys:acl-user nodes) 474 * @param user The user 475 * @param rootPath The root path to restrict the search. Can be null. 476 * @param predicat The predicat expression. Can be null. 477 * @return The ACL user objects for user 478 */ 479 public static NodeIterator getACLUsers (UserIdentity user, String rootPath, Expression predicat) 480 { 481 StringBuilder sb = new StringBuilder("/jcr:root"); 482 483 if (rootPath != null) 484 { 485 sb.append(rootPath); 486 } 487 488 sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")") 489 .append("/").append(__NODE_NAME_ACL_USERS) 490 .append("/").append(user.getPopulationId()) 491 .append("/").append(ISO9075.encode(user.getLogin())); 492 493 if (predicat != null) 494 { 495 sb.append("[").append(predicat.build()).append("]"); 496 } 497 498 String jcrQuery = sb.toString(); 499 return _query(jcrQuery); 500 } 501 502 /** 503 * Returns all ACL objects for users user (ametys:acl-user nodes) 504 * @param rootPath The root path to restrict the search. Can be null. 505 * @param predicate The predicate expression. Can be null. 506 * @return The ACL user objects for users 507 */ 508 public static NodeIterator getACLUsers (String rootPath, Expression predicate) 509 { 510 StringBuilder sb = new StringBuilder("/jcr:root"); 511 512 if (rootPath != null) 513 { 514 sb.append(rootPath); 515 } 516 517 sb.append("//element(*, ").append(__NODETYPE_ACL_USER).append(")"); 518 519 if (predicate != null) 520 { 521 sb.append("[").append(predicate.build()).append("]"); 522 } 523 524 String jcrQuery = sb.toString(); 525 return _query(jcrQuery); 526 } 527 528 /** 529 * Returns all ACL objects for users (ametys:acl-user nodes) 530 * @param predicat The predicat expression. Can be null. 531 * @return The ACL user objects for users 532 */ 533 public static NodeIterator getACLUsers (Expression predicat) 534 { 535 StringBuilder sb = new StringBuilder(); 536 537 sb.append("//element(*, ").append(__NODETYPE_ACL_USER).append(")"); 538 539 if (predicat != null) 540 { 541 sb.append("[").append(predicat.build()).append("]"); 542 } 543 544 return _query(sb.toString()); 545 } 546 547 /** 548 * Returns all ACL objects for groups (ametys:acl-group nodes) 549 * @param predicat The predicat expression. Can be null. 550 * @return The ACL group objects for groups 551 */ 552 public static NodeIterator getACLGroups (Expression predicat) 553 { 554 StringBuilder sb = new StringBuilder(); 555 556 sb.append("//element(*, ").append(__NODETYPE_ACL_GROUP).append(")"); 557 558 if (predicat != null) 559 { 560 sb.append("[").append(predicat.build()).append("]"); 561 } 562 563 return _query(sb.toString()); 564 } 565 566 /** 567 * Returns all ACL objects for groups (ametys:acl-group nodes) 568 * @param rootPath The root path to restrict the search. Can be null. 569 * @param predicate The predicate expression. Can be null. 570 * @return The ACL group objects for groups 571 */ 572 public static NodeIterator getACLGroups (String rootPath, Expression predicate) 573 { 574 StringBuilder sb = new StringBuilder("/jcr:root"); 575 576 if (rootPath != null) 577 { 578 sb.append(rootPath); 579 } 580 581 sb.append("//element(*, ").append(__NODETYPE_ACL_GROUP).append(")"); 582 583 if (predicate != null) 584 { 585 sb.append("[").append(predicate.build()).append("]"); 586 } 587 588 return _query(sb.toString()); 589 } 590 591 private static NodeIterator _getApprochingACLGroups (Set<GroupIdentity> groups, String rootPath, Expression predicat) 592 { 593 StringBuilder sb = new StringBuilder("/jcr:root"); 594 595 if (rootPath != null) 596 { 597 sb.append(rootPath); 598 } 599 600 sb.append("//element(*, ametys:acl-group)[("); 601 602 sb.append(groups.stream() 603 .map(GroupIdentity::getId) 604 .map(Text::escapeIllegalJcrChars) 605 .map(ISO9075::encode) // used to support nodeName with number (id of SQL Group) 606 .map(nodeName -> "fn:name()='" + nodeName + "'") 607 .collect(Collectors.joining(LogicalOperator.OR.toString()))); 608 sb.append(")"); 609 610 if (predicat != null) 611 { 612 sb.append(LogicalOperator.AND.toString()).append(predicat.build()); 613 } 614 615 sb.append("]"); 616 617 return _query(sb.toString()); 618 } 619 620 /** 621 * Returns all ACL objects for a given group (ametys:acl-group nodes) 622 * @param group The group 623 * @param rootPath The root path to restrict the search. Can be null. 624 * @param predicat The predicat expression. Can be null. 625 * @return The ACL user objects for groups 626 */ 627 public static NodeIterator getACLGroups (GroupIdentity group, String rootPath, Expression predicat) 628 { 629 StringBuilder sb = new StringBuilder("/jcr:root"); 630 631 if (rootPath != null) 632 { 633 sb.append(rootPath); 634 } 635 636 sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")") 637 .append("/").append(__NODE_NAME_ACL_GROUPS) 638 .append("/").append(group.getDirectoryId()) 639 .append("/").append(ISO9075.encode(Text.escapeIllegalJcrChars(group.getId()))); 640 641 if (predicat != null) 642 { 643 sb.append("[").append(predicat.build()).append("]"); 644 } 645 646 return _query(sb.toString()); 647 } 648 649 private static NodeIterator _query (String jcrQuery) 650 { 651 Session session = null; 652 try 653 { 654 session = _repository.login(); 655 long t1 = System.currentTimeMillis(); 656 @SuppressWarnings("deprecation") 657 Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH); 658 if (_logger.isInfoEnabled()) 659 { 660 _logger.info("ACLJCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms"); 661 } 662 return query.execute().getNodes(); 663 } 664 catch (RepositoryException ex) 665 { 666 if (session != null) 667 { 668 session.logout(); 669 } 670 throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex); 671 } 672 } 673 674 /* ------------------------------------------- */ 675 /* PROFILES FOR ANY CONNECTED USER / ANONYMOUS */ 676 /* ------------------------------------------- */ 677 678 /** 679 * Helper for {@link ACLAmetysObject#getProfilesForAnonymousAndAnyConnectedUser} 680 * @param node The JCR node for the Ametys object 681 * @return a map containing allowed/denied profiles that anonymous and any connected user has on the given object 682 */ 683 public static Map<AnonymousOrAnyConnectedKeys, Set<String>> getProfilesForAnonymousAndAnyConnectedUser(Node node) 684 { 685 Node aclNode = _getACLNode(node); 686 if (aclNode == null) 687 { 688 return __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT; 689 } 690 else 691 { 692 return Map.of(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES), 693 AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES), 694 AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES), 695 AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES)); 696 } 697 } 698 699 /** 700 * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnyConnectedUser(Set)} 701 * @param node The JCR node for the Ametys object 702 * @param profileIds The profiles to add 703 */ 704 public static void addAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds) 705 { 706 Node aclNode = _getOrCreateACLNode(node); 707 for (String profile : profileIds) 708 { 709 _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile); 710 } 711 _save(node); 712 } 713 714 /** 715 * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnyConnectedUser(Set)} 716 * @param node The JCR node for the Ametys object 717 * @param profileIds The profiles to remove 718 */ 719 public static void removeAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds) 720 { 721 Node aclNode = _getOrCreateACLNode(node); 722 for (String profile : profileIds) 723 { 724 _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile); 725 } 726 _save(node); 727 } 728 729 /** 730 * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)} 731 * @param node The JCR node for the Ametys object 732 * @param profileIds The profiles to add 733 */ 734 public static void addDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds) 735 { 736 Node aclNode = _getOrCreateACLNode(node); 737 for (String profile : profileIds) 738 { 739 _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile); 740 } 741 _save(node); 742 } 743 744 /** 745 * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)} 746 * @param node The JCR node for the Ametys object 747 * @param profileIds The profiles to remove 748 */ 749 public static void removeDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds) 750 { 751 Node aclNode = _getOrCreateACLNode(node); 752 for (String profile : profileIds) 753 { 754 _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile); 755 } 756 _save(node); 757 } 758 759 /** 760 * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnonymous(Set)} 761 * @param node The JCR node for the Ametys object 762 * @param profileIds The profiles to add 763 */ 764 public static void addAllowedProfilesForAnonymous(Node node, Set<String> profileIds) 765 { 766 Node aclNode = _getOrCreateACLNode(node); 767 for (String profile : profileIds) 768 { 769 _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile); 770 } 771 _save(node); 772 } 773 774 /** 775 * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnonymous(Set)} 776 * @param node The JCR node for the Ametys object 777 * @param profileIds The profiles to remove 778 */ 779 public static void removeAllowedProfilesForAnonymous(Node node, Set<String> profileIds) 780 { 781 Node aclNode = _getOrCreateACLNode(node); 782 for (String profile : profileIds) 783 { 784 _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile); 785 } 786 _save(node); 787 } 788 789 /** 790 * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)} 791 * @param node The JCR node for the Ametys object 792 * @param profileIds The profiles to add 793 */ 794 public static void addDeniedProfilesForAnonymous(Node node, Set<String> profileIds) 795 { 796 Node aclNode = _getOrCreateACLNode(node); 797 for (String profile : profileIds) 798 { 799 _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile); 800 } 801 _save(node); 802 } 803 804 /** 805 * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)} 806 * @param node The JCR node for the Ametys object 807 * @param profileIds The profiles to remove 808 */ 809 public static void removeDeniedProfilesForAnonymous(Node node, Set<String> profileIds) 810 { 811 Node aclNode = _getOrCreateACLNode(node); 812 for (String profile : profileIds) 813 { 814 _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile); 815 } 816 _save(node); 817 } 818 819 820 /* ------------------- */ 821 /* MANAGEMENT OF USERS */ 822 /* ------------------- */ 823 /** 824 * Helper for {@link ACLAmetysObject#getProfilesForUsers} 825 * @param node The JCR node for the Ametys object 826 * @param user The user to get profiles for. Can be null to get profiles for all users that have rights 827 * @return The map of allowed users with their assigned allowed/denied profiles 828 */ 829 public static Map<UserIdentity, Map<UserOrGroup, Set<String>>> getProfilesForUsers(Node node, UserIdentity user) 830 { 831 if (user == null) 832 { 833 try 834 { 835 Node usersNode = _getUsersNode(node); 836 if (usersNode == null) 837 { 838 return Map.of(); 839 } 840 841 Map<UserIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>(); 842 843 NodeIterator populationsIterator = usersNode.getNodes(); 844 while (populationsIterator.hasNext()) 845 { 846 Node populationNode = populationsIterator.nextNode(); 847 NodeIterator usersIterator = populationNode.getNodes(); 848 while (usersIterator.hasNext()) 849 { 850 Node userNode = usersIterator.nextNode(); 851 Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES); 852 Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES); 853 if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty()) 854 { 855 result.put(new UserIdentity(userNode.getName(), populationNode.getName()), 856 Map.of(UserOrGroup.ALLOWED, allowedProfiles, 857 UserOrGroup.DENIED, deniedProfiles)); 858 } 859 } 860 } 861 862 return result; 863 } 864 catch (RepositoryException e) 865 { 866 throw new AmetysRepositoryException("Unable to get allowed/denied users", e); 867 } 868 } 869 else 870 { 871 Node userNode = _getUserNode(node, user); 872 if (userNode == null) 873 { 874 return Map.of(user, __USER_OR_GROUP_NORIGHT); 875 } 876 else 877 { 878 return Map.of(user, 879 Map.of(UserOrGroup.ALLOWED, _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES), 880 UserOrGroup.DENIED, _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES))); 881 } 882 } 883 } 884 885 /** 886 * Helper for {@link ModifiableACLAmetysObject#addAllowedUsers(Set, String)} 887 * @param users The users to add 888 * @param node The JCR node for the Ametys object 889 * @param profileId The id of the profile 890 */ 891 public static void addAllowedUsers(Set<UserIdentity> users, Node node, String profileId) 892 { 893 for (UserIdentity userIdentity : users) 894 { 895 Node userNode = _getOrCreateUserNode(node, userIdentity); 896 _addProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId); 897 } 898 _save(node); 899 } 900 901 /** 902 * Helper for {@link ModifiableACLAmetysObject#removeAllowedUsers(Set, String)} 903 * @param users The users to remove 904 * @param node The JCR node for the Ametys object 905 * @param profileId The id of the profile 906 */ 907 public static void removeAllowedUsers(Set<UserIdentity> users, Node node, String profileId) 908 { 909 for (UserIdentity userIdentity : users) 910 { 911 Node userNode = _getOrCreateUserNode(node, userIdentity); 912 _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId); 913 } 914 _save(node); 915 } 916 917 /** 918 * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)} 919 * @param users The users to remove 920 * @param node The JCR node for the Ametys object 921 */ 922 public static void removeAllowedUsers(Set<UserIdentity> users, Node node) 923 { 924 for (UserIdentity userIdentity : users) 925 { 926 Node userNode = _getOrCreateUserNode(node, userIdentity); 927 _setProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET); 928 } 929 _save(node); 930 } 931 932 /** 933 * Helper for {@link ModifiableACLAmetysObject#addDeniedUsers(Set, String)} 934 * @param users The users to add 935 * @param node The JCR node for the Ametys object 936 * @param profileId The id of the profile 937 */ 938 public static void addDeniedUsers(Set<UserIdentity> users, Node node, String profileId) 939 { 940 for (UserIdentity userIdentity : users) 941 { 942 Node userNode = _getOrCreateUserNode(node, userIdentity); 943 _addProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId); 944 } 945 _save(node); 946 } 947 948 /** 949 * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set, String)} 950 * @param users The users to remove 951 * @param node The JCR node for the Ametys object 952 * @param profileId The id of the profile 953 */ 954 public static void removeDeniedUsers(Set<UserIdentity> users, Node node, String profileId) 955 { 956 for (UserIdentity userIdentity : users) 957 { 958 Node userNode = _getOrCreateUserNode(node, userIdentity); 959 _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId); 960 } 961 _save(node); 962 } 963 964 /** 965 * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set)} 966 * @param users The users to remove 967 * @param node The JCR node for the Ametys object 968 */ 969 public static void removeDeniedUsers(Set<UserIdentity> users, Node node) 970 { 971 for (UserIdentity userIdentity : users) 972 { 973 Node userNode = _getOrCreateUserNode(node, userIdentity); 974 _setProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET); 975 } 976 _save(node); 977 } 978 979 /* -------------------- */ 980 /* MANAGEMENT OF GROUPS */ 981 /* -------------------- */ 982 983 /** 984 * Helper for {@link ACLAmetysObject#getProfilesForGroups} 985 * @param node The JCR node for the Ametys object 986 * @param groups The group to get profiles for. Can be null to get profiles for all groups that have rights 987 * @return The map of allowed/denied groups with their assigned profiles 988 */ 989 public static Map<GroupIdentity, Map<UserOrGroup, Set<String>>> getProfilesForGroups(Node node, Set<GroupIdentity> groups) 990 { 991 try 992 { 993 if (groups != null && groups.isEmpty()) 994 { 995 return Map.of(); 996 } 997 998 Node groupsNode = _getGroupsNode(node); 999 if (groupsNode == null) 1000 { 1001 return Map.of(); 1002 } 1003 1004 Map<GroupIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>(); 1005 1006 if (groups == null) 1007 { 1008 NodeIterator groupDirectoriesIterator = groupsNode.getNodes(); 1009 while (groupDirectoriesIterator.hasNext()) 1010 { 1011 Node groupDirectoryNode = groupDirectoriesIterator.nextNode(); 1012 NodeIterator groupsIterator = groupDirectoryNode.getNodes(); 1013 while (groupsIterator.hasNext()) 1014 { 1015 Node groupNode = groupsIterator.nextNode(); 1016 Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES); 1017 Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES); 1018 if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty()) 1019 { 1020 result.put(new GroupIdentity(Text.unescapeIllegalJcrChars(groupNode.getName()), groupDirectoryNode.getName()), 1021 Map.of(UserOrGroup.ALLOWED, allowedProfiles, 1022 UserOrGroup.DENIED, deniedProfiles)); 1023 } 1024 } 1025 } 1026 } 1027 else 1028 { 1029 Map<String, Node> groupsNodeByDirectoryIdCache = new HashMap<>(); 1030 1031 for (GroupIdentity group : groups) 1032 { 1033 Node directoryNode = groupsNodeByDirectoryIdCache.computeIfAbsent(group.getDirectoryId(), LambdaUtils.wrap(directoryId -> groupsNode.hasNode(directoryId) ? groupsNode.getNode(directoryId) : null)); 1034 1035 String groupNodeName = Text.escapeIllegalJcrChars(group.getId()); 1036 if (directoryNode != null && directoryNode.hasNode(groupNodeName)) 1037 { 1038 Node groupNode = directoryNode.getNode(groupNodeName); 1039 1040 result.put(group, 1041 Map.of(UserOrGroup.ALLOWED, _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES), 1042 UserOrGroup.DENIED, _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES))); 1043 } 1044 } 1045 } 1046 1047 return result; 1048 } 1049 catch (RepositoryException e) 1050 { 1051 throw new AmetysRepositoryException("Unable to get allowed/denied groups", e); 1052 } 1053 } 1054 1055 /** 1056 * Helper for {@link ModifiableACLAmetysObject#addAllowedGroups(Set, String)} 1057 * @param groups The groups to add 1058 * @param node The JCR node for the Ametys object 1059 * @param profileId The id of the profile 1060 */ 1061 public static void addAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId) 1062 { 1063 for (GroupIdentity groupIdentity : groups) 1064 { 1065 Node groupNode = _getOrCreateGroupNode(node, groupIdentity); 1066 _addProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId); 1067 } 1068 _save(node); 1069 } 1070 1071 /** 1072 * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set, String)} 1073 * @param groups The groups to remove 1074 * @param node The JCR node for the Ametys object 1075 * @param profileId The id of the profile 1076 */ 1077 public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId) 1078 { 1079 for (GroupIdentity groupIdentity : groups) 1080 { 1081 Node groupNode = _getOrCreateGroupNode(node, groupIdentity); 1082 _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId); 1083 } 1084 _save(node); 1085 } 1086 1087 /** 1088 * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)} 1089 * @param groups The groups to remove 1090 * @param node The JCR node for the Ametys object 1091 */ 1092 public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node) 1093 { 1094 for (GroupIdentity groupIdentity : groups) 1095 { 1096 Node groupNode = _getOrCreateGroupNode(node, groupIdentity); 1097 _setProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET); 1098 } 1099 _save(node); 1100 } 1101 1102 /** 1103 * Helper for {@link ModifiableACLAmetysObject#addDeniedGroups(Set, String)} 1104 * @param groups The groups to add 1105 * @param node The JCR node for the Ametys object 1106 * @param profileId The id of the profile 1107 */ 1108 public static void addDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId) 1109 { 1110 for (GroupIdentity groupIdentity : groups) 1111 { 1112 Node groupNode = _getOrCreateGroupNode(node, groupIdentity); 1113 _addProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId); 1114 } 1115 _save(node); 1116 } 1117 1118 /** 1119 * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set, String)} 1120 * @param groups The groups to remove 1121 * @param node The JCR node for the Ametys object 1122 * @param profileId The id of the profile 1123 */ 1124 public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId) 1125 { 1126 for (GroupIdentity groupIdentity : groups) 1127 { 1128 Node groupNode = _getOrCreateGroupNode(node, groupIdentity); 1129 _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId); 1130 } 1131 _save(node); 1132 } 1133 1134 /** 1135 * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set)} 1136 * @param groups The groups to remove 1137 * @param node The JCR node for the Ametys object 1138 */ 1139 public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node) 1140 { 1141 for (GroupIdentity groupIdentity : groups) 1142 { 1143 Node groupNode = _getOrCreateGroupNode(node, groupIdentity); 1144 _setProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET); 1145 } 1146 _save(node); 1147 } 1148 1149 1150 /* ------ */ 1151 /* REMOVE */ 1152 /* ------ */ 1153 1154 /** 1155 * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeProfile(String)} 1156 * @param profileId The id of the profile 1157 */ 1158 public static void removeProfile(String profileId) 1159 { 1160 // Remove this profile set as allowed or denied in users 1161 Expression expr = new OrExpression(new AllowedProfileExpression(profileId), new DeniedProfileExpression(profileId)); 1162 NodeIterator users = getACLUsers(expr); 1163 while (users.hasNext()) 1164 { 1165 Node userNode = (Node) users.next(); 1166 _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId); 1167 _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId); 1168 _save(userNode); 1169 } 1170 1171 // Remove this profile set as allowed or denied in groups 1172 NodeIterator groups = getACLGroups(expr); 1173 while (groups.hasNext()) 1174 { 1175 Node groupNode = (Node) groups.next(); 1176 _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId); 1177 _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId); 1178 _save(groupNode); 1179 } 1180 1181 // Remove this profile set as allowed or denied for anonymous and any connected 1182 expr = new OrExpression(new AnonymousAllowedProfileExpression(profileId), new AnonymousDeniedProfileExpression(profileId), new AnyConnectedAllowedProfileExpression(profileId), new AnyConnectedDeniedProfileExpression(profileId)); 1183 NodeIterator nodes = getACLRoots(null, expr); 1184 while (nodes.hasNext()) 1185 { 1186 Node node = (Node) nodes.next(); 1187 _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileId); 1188 _removeProperty(node, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileId); 1189 _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileId); 1190 _removeProperty(node, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileId); 1191 _save(node); 1192 } 1193 } 1194 1195 /** 1196 * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeUser(UserIdentity)} 1197 * @param user The user 1198 */ 1199 public static void removeUser(UserIdentity user) 1200 { 1201 NodeIterator users = getACLUsers(user, null, null); 1202 1203 while (users.hasNext()) 1204 { 1205 Node userNode = (Node) users.next(); 1206 try 1207 { 1208 userNode.remove(); 1209 _save(userNode); 1210 } 1211 catch (RepositoryException e) 1212 { 1213 throw new AmetysRepositoryException(e); 1214 } 1215 } 1216 } 1217 1218 /** 1219 * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeGroup(GroupIdentity)} 1220 * @param group The group 1221 */ 1222 public static void removeGroup(GroupIdentity group) 1223 { 1224 NodeIterator groups = getACLGroups(group, null, null); 1225 while (groups.hasNext()) 1226 { 1227 Node gpNode = (Node) groups.next(); 1228 try 1229 { 1230 gpNode.remove(); 1231 _save(gpNode); 1232 } 1233 catch (RepositoryException e) 1234 { 1235 throw new AmetysRepositoryException(e); 1236 } 1237 } 1238 } 1239 1240 /* --------------- */ 1241 /* INHERITANCE */ 1242 /* --------------- */ 1243 /** 1244 * Helper for {@link ACLAmetysObject#isInheritanceDisallowed()} 1245 * @param node The JCR node for the Ametys object 1246 * @return true if the inheritance is disallow of the given node 1247 */ 1248 public static boolean isInheritanceDisallowed(Node node) 1249 { 1250 try 1251 { 1252 Node aclNode = _getACLNode(node); 1253 if (aclNode != null && aclNode.hasProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE)) 1254 { 1255 return aclNode.getProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE).getBoolean(); 1256 } 1257 return false; 1258 } 1259 catch (RepositoryException e) 1260 { 1261 throw new AmetysRepositoryException("Unable to get " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e); 1262 } 1263 } 1264 1265 /** 1266 * Helper for {@link ModifiableACLAmetysObject#disallowInheritance(boolean)} 1267 * @param node The JCR node for the Ametys object 1268 * @param disallow true to disallow the inheritance, false otherwise 1269 */ 1270 public static void disallowInheritance(Node node, boolean disallow) 1271 { 1272 Node aclNode = _getOrCreateACLNode(node); 1273 try 1274 { 1275 aclNode.setProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE, disallow); 1276 } 1277 catch (RepositoryException e) 1278 { 1279 throw new AmetysRepositoryException("Unable to set " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e); 1280 } 1281 _save(node); 1282 } 1283 1284 1285 /* --------------- */ 1286 /* PRIVATE METHODS */ 1287 /* --------------- */ 1288 1289 private static void _checkLock(Node node) throws AmetysRepositoryException 1290 { 1291 try 1292 { 1293 if (node.isLocked()) 1294 { 1295 LockManager lockManager = node.getSession().getWorkspace().getLockManager(); 1296 1297 Lock lock = lockManager.getLock(node.getPath()); 1298 Node lockHolder = lock.getNode(); 1299 1300 lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString()); 1301 } 1302 } 1303 catch (RepositoryException e) 1304 { 1305 throw new AmetysRepositoryException("Unable to add lock token on ACL node", e); 1306 } 1307 } 1308 1309 private static Node _getOrCreateACLNode(Node node) 1310 { 1311 try 1312 { 1313 if (node.hasNode(__NODE_NAME_ROOT_ACL)) 1314 { 1315 return node.getNode(__NODE_NAME_ROOT_ACL); 1316 } 1317 else 1318 { 1319 _checkLock(node); 1320 return node.addNode(__NODE_NAME_ROOT_ACL, __NODETYPE_ROOT_ACL); 1321 } 1322 } 1323 catch (RepositoryException e) 1324 { 1325 throw new AmetysRepositoryException("Error while getting root ACL node.", e); 1326 } 1327 } 1328 1329 private static Node _getACLNode(Node node) 1330 { 1331 try 1332 { 1333 if (node.hasNode(__NODE_NAME_ROOT_ACL)) 1334 { 1335 return node.getNode(__NODE_NAME_ROOT_ACL); 1336 } 1337 else 1338 { 1339 return null; 1340 } 1341 } 1342 catch (RepositoryException e) 1343 { 1344 throw new AmetysRepositoryException("Error while getting root ACL node.", e); 1345 } 1346 } 1347 1348 private static Node _getOrCreateUsersNode(Node node) 1349 { 1350 try 1351 { 1352 Node aclNode = _getOrCreateACLNode(node); 1353 if (aclNode.hasNode(__NODE_NAME_ACL_USERS)) 1354 { 1355 return aclNode.getNode(__NODE_NAME_ACL_USERS); 1356 } 1357 else 1358 { 1359 return aclNode.addNode(__NODE_NAME_ACL_USERS, JcrConstants.NT_UNSTRUCTURED); 1360 } 1361 } 1362 catch (RepositoryException e) 1363 { 1364 throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e); 1365 } 1366 } 1367 1368 private static Node _getUserNode(Node node, UserIdentity user) 1369 { 1370 try 1371 { 1372 Node aclNode = _getACLNode(node); 1373 if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS)) 1374 { 1375 Node aclUsersNode = aclNode.getNode(__NODE_NAME_ACL_USERS); 1376 if (aclUsersNode.hasNode(user.getPopulationId())) 1377 { 1378 Node popNode = aclUsersNode.getNode(user.getPopulationId()); 1379 if (popNode.hasNode(user.getLogin())) 1380 { 1381 return popNode.getNode(user.getLogin()); 1382 } 1383 } 1384 } 1385 1386 return null; 1387 } 1388 catch (RepositoryException e) 1389 { 1390 throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e); 1391 } 1392 } 1393 1394 private static Node _getUsersNode(Node node) 1395 { 1396 try 1397 { 1398 Node aclNode = _getACLNode(node); 1399 if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS)) 1400 { 1401 return aclNode.getNode(__NODE_NAME_ACL_USERS); 1402 } 1403 else 1404 { 1405 return null; 1406 } 1407 } 1408 catch (RepositoryException e) 1409 { 1410 throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e); 1411 } 1412 } 1413 1414 private static Node _getOrCreateGroupsNode(Node node) 1415 { 1416 try 1417 { 1418 Node aclNode = _getOrCreateACLNode(node); 1419 if (aclNode.hasNode(__NODE_NAME_ACL_GROUPS)) 1420 { 1421 return aclNode.getNode(__NODE_NAME_ACL_GROUPS); 1422 } 1423 else 1424 { 1425 return aclNode.addNode(__NODE_NAME_ACL_GROUPS, JcrConstants.NT_UNSTRUCTURED); 1426 } 1427 } 1428 catch (RepositoryException e) 1429 { 1430 throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e); 1431 } 1432 } 1433 1434 private static Node _getGroupsNode(Node node) 1435 { 1436 try 1437 { 1438 Node aclNode = _getACLNode(node); 1439 if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_GROUPS)) 1440 { 1441 return aclNode.getNode(__NODE_NAME_ACL_GROUPS); 1442 } 1443 else 1444 { 1445 return null; 1446 } 1447 } 1448 catch (RepositoryException e) 1449 { 1450 throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e); 1451 } 1452 } 1453 1454 private static Node _getOrCreateUserNode(Node node, UserIdentity userIdentity) 1455 { 1456 try 1457 { 1458 Node usersNode = _getOrCreateUsersNode(node); 1459 String population = userIdentity.getPopulationId(); 1460 String login = userIdentity.getLogin(); 1461 1462 if (usersNode.hasNode(population)) 1463 { 1464 Node populationNode = usersNode.getNode(population); 1465 if (populationNode.hasNode(login)) 1466 { 1467 return populationNode.getNode(login); 1468 } 1469 else 1470 { 1471 return populationNode.addNode(login, __NODETYPE_ACL_USER); 1472 } 1473 } 1474 else 1475 { 1476 return usersNode.addNode(population, JcrConstants.NT_UNSTRUCTURED).addNode(login, __NODETYPE_ACL_USER); 1477 } 1478 } 1479 catch (RepositoryException e) 1480 { 1481 throw new AmetysRepositoryException(String.format("Error while getting 'user' ACL node for %s.", userIdentity.toString()), e); 1482 } 1483 } 1484 1485 private static Node _getOrCreateGroupNode(Node node, GroupIdentity groupIdentity) 1486 { 1487 try 1488 { 1489 Node groupsNode = _getOrCreateGroupsNode(node); 1490 String directoryId = groupIdentity.getDirectoryId(); 1491 String id = Text.escapeIllegalJcrChars(groupIdentity.getId()); 1492 1493 if (groupsNode.hasNode(directoryId)) 1494 { 1495 Node populationNode = groupsNode.getNode(directoryId); 1496 if (populationNode.hasNode(id)) 1497 { 1498 return populationNode.getNode(id); 1499 } 1500 else 1501 { 1502 return populationNode.addNode(id, __NODETYPE_ACL_GROUP); 1503 } 1504 } 1505 else 1506 { 1507 return groupsNode.addNode(directoryId, JcrConstants.NT_UNSTRUCTURED).addNode(id, __NODETYPE_ACL_GROUP); 1508 } 1509 } 1510 catch (RepositoryException e) 1511 { 1512 throw new AmetysRepositoryException(String.format("Error while getting 'group' ACL node for %s.", groupIdentity.toString()), e); 1513 } 1514 } 1515 1516 private static Set<String> _getProperty(Node node, String propertyName) 1517 { 1518 try 1519 { 1520 Value[] values = node.getProperty(propertyName).getValues(); 1521 Set<String> result = new HashSet<>(); 1522 for (Value value : values) 1523 { 1524 result.add(value.getString()); 1525 } 1526 return result; 1527 } 1528 catch (PathNotFoundException e) 1529 { 1530 return new HashSet<>(); 1531 } 1532 catch (RepositoryException e) 1533 { 1534 throw new AmetysRepositoryException("Unable to get " + propertyName + " property", e); 1535 } 1536 } 1537 1538 private static void _setProperty(Node node, String propertyName, Set<String> profiles) 1539 { 1540 try 1541 { 1542 node.setProperty(propertyName, profiles.toArray(new String[profiles.size()])); 1543 } 1544 catch (RepositoryException e) 1545 { 1546 throw new AmetysRepositoryException("Unable to set " + propertyName + " property", e); 1547 } 1548 } 1549 1550 private static void _addProperty(Node node, String propertyName, String profileToAdd) 1551 { 1552 Set<String> profiles = _getProperty(node, propertyName); 1553 if (!profiles.contains(profileToAdd)) 1554 { 1555 profiles.add(profileToAdd); 1556 _setProperty(node, propertyName, profiles); 1557 } 1558 } 1559 1560 private static void _removeProperty(Node node, String propertyName, String profileToRemove) 1561 { 1562 Set<String> profiles = _getProperty(node, propertyName); 1563 if (profiles.contains(profileToRemove)) 1564 { 1565 profiles.remove(profileToRemove); 1566 _setProperty(node, propertyName, profiles); 1567 } 1568 } 1569 1570 private static void _save(Node node) 1571 { 1572 try 1573 { 1574 node.getSession().save(); 1575 } 1576 catch (RepositoryException e) 1577 { 1578 throw new AmetysRepositoryException("Unable to save changes", e); 1579 } 1580 } 1581 1582 /* ---------------------------------------*/ 1583 /* JCR EXPRESSIONS FOR PROFILES */ 1584 /* ---------------------------------------*/ 1585 1586 static class AllowedProfileExpression extends ACLProfileExpression 1587 { 1588 public AllowedProfileExpression (String ... profileIds) 1589 { 1590 super(__PROPERTY_NAME_ALLOWED_PROFILES, profileIds); 1591 } 1592 } 1593 1594 static class DeniedProfileExpression extends ACLProfileExpression 1595 { 1596 public DeniedProfileExpression (String ... profileIds) 1597 { 1598 super(__PROPERTY_NAME_DENIED_PROFILES, profileIds); 1599 } 1600 } 1601 1602 static class AnyConnectedDeniedProfileExpression extends ACLProfileExpression 1603 { 1604 public AnyConnectedDeniedProfileExpression (String ... profileIds) 1605 { 1606 super(__PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileIds); 1607 } 1608 } 1609 1610 static class AnyConnectedAllowedProfileExpression extends ACLProfileExpression 1611 { 1612 public AnyConnectedAllowedProfileExpression (String ... profileIds) 1613 { 1614 super(__PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileIds); 1615 } 1616 } 1617 1618 static class AnonymousDeniedProfileExpression extends ACLProfileExpression 1619 { 1620 public AnonymousDeniedProfileExpression (String ... profileIds) 1621 { 1622 super(__PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileIds); 1623 } 1624 } 1625 1626 static class AnonymousAllowedProfileExpression extends ACLProfileExpression 1627 { 1628 public AnonymousAllowedProfileExpression (String ... profileIds) 1629 { 1630 super(__PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileIds); 1631 } 1632 } 1633 1634 static class ACLProfileExpression implements Expression 1635 { 1636 private String[] _profileIds; 1637 private String _propertyName; 1638 1639 public ACLProfileExpression (String propertyName, String ... profileIds) 1640 { 1641 _propertyName = propertyName; 1642 _profileIds = profileIds; 1643 } 1644 1645 @Override 1646 public String build() 1647 { 1648 boolean isFirst = true; 1649 StringBuilder sb = new StringBuilder("("); 1650 1651 for (String profileId : _profileIds) 1652 { 1653 if (isFirst) 1654 { 1655 isFirst = false; 1656 } 1657 else 1658 { 1659 sb.append(LogicalOperator.OR.toString()); 1660 } 1661 1662 sb.append("@") 1663 .append(_propertyName) 1664 .append(Operator.EQ) 1665 .append("'").append(profileId).append("'"); 1666 } 1667 1668 if (isFirst) 1669 { 1670 return ""; 1671 } 1672 else 1673 { 1674 return sb.append(")").toString(); 1675 } 1676 } 1677 } 1678 1679 /** 1680 * Get all contexts with a permission for the profile for anonymous or any connected user 1681 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query 1682 * @param profileId the profile id 1683 * @return a map of the permissions for each context 1684 */ 1685 public static Map<Object, Set<AnonymousOrAnyConnectedKeys>> getProfilePermissionsForAnonymousAndAnyConnectedUser(Set< ? extends Object> rootNodes, String profileId) 1686 { 1687 Expression expr = new OrExpression( 1688 new AnyConnectedAllowedProfileExpression(profileId), 1689 new AnyConnectedDeniedProfileExpression(profileId), 1690 new AnonymousAllowedProfileExpression(profileId), 1691 new AnonymousDeniedProfileExpression(profileId) 1692 ); 1693 1694 Map<Object, Set<AnonymousOrAnyConnectedKeys>> result = new HashMap<>(); 1695 for (String rootPath : _convertNodeToPath(rootNodes)) 1696 { 1697 NodeIterator nodes = getACLRoots(rootPath, expr); 1698 1699 // Only retrieve node with assignments to anonymous or any connected 1700 while (nodes.hasNext()) 1701 { 1702 Node aclNode = nodes.nextNode(); 1703 try 1704 { 1705 Set<AnonymousOrAnyConnectedKeys> aoResult = new HashSet<>(); 1706 Set<String> allowedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES); 1707 if (allowedAnonymous.contains(profileId)) 1708 { 1709 aoResult.add(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED); 1710 } 1711 1712 Set<String> deniedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES); 1713 if (deniedAnonymous.contains(profileId)) 1714 { 1715 aoResult.add(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED); 1716 } 1717 Set<String> allowedAny = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES); 1718 if (allowedAny.contains(profileId)) 1719 { 1720 aoResult.add(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED); 1721 } 1722 Set<String> deniedAny = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES); 1723 if (deniedAny.contains(profileId)) 1724 { 1725 aoResult.add(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED); 1726 } 1727 1728 if (!aoResult.isEmpty()) 1729 { 1730 AmetysObject ao = _getAmetysObjectFromACLNode(aclNode); 1731 result.put(ao, aoResult); 1732 } 1733 } 1734 catch (RepositoryException e) 1735 { 1736 _logger.error("Failed to retrieve object for acl node " + aclNode.toString() + ". The node will be ignored."); 1737 } 1738 } 1739 } 1740 1741 return result; 1742 } 1743 1744 /** 1745 * Get all context with a permission for the profile for a group 1746 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query 1747 * @param profileId the profile id 1748 * @return a map of the group's permission for each context 1749 */ 1750 public static Map<Object, Map<GroupIdentity, Set<UserOrGroup>>> getProfilePermissionsForGroups(Set< ? extends Object> rootNodes, String profileId) 1751 { 1752 Expression expr = new OrExpression( 1753 new AllowedProfileExpression(profileId), 1754 new DeniedProfileExpression(profileId) 1755 ); 1756 1757 Map<Object, Map<GroupIdentity, Set<UserOrGroup>>> result = new HashMap<>(); 1758 for (String rootPath : _convertNodeToPath(rootNodes)) 1759 { 1760 NodeIterator nodes = getACLGroups(rootPath, expr); 1761 1762 while (nodes.hasNext()) 1763 { 1764 Node groupNode = nodes.nextNode(); 1765 try 1766 { 1767 String groupId = Text.unescapeIllegalJcrChars(groupNode.getName()); 1768 String directoryId = groupNode.getParent().getName(); 1769 1770 GroupIdentity currentGroup = new GroupIdentity(groupId, directoryId); 1771 1772 // Determine the group permissions 1773 Set<UserOrGroup> permission = new HashSet<>(); 1774 1775 if (_getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES).contains(profileId)) 1776 { 1777 permission.add(UserOrGroup.DENIED); 1778 } 1779 else if (_getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES).contains(profileId)) 1780 { 1781 permission.add(UserOrGroup.ALLOWED); 1782 } 1783 1784 // Only add actual permissions to the result 1785 if (!permission.isEmpty()) 1786 { 1787 AmetysObject ao = _getAmetysObjectFromACLNode(groupNode); 1788 // The ametys object could already be in the result map having permissions from an other group 1789 Map<GroupIdentity, Set<UserOrGroup>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>()); 1790 // There can only be one node per group, so we don't need to retrieve existing value 1791 objectPermissions.put(currentGroup, permission); 1792 } 1793 } 1794 catch (RepositoryException e) 1795 { 1796 _logger.error("Failed to retrieve object for group acl node " + groupNode.toString() + ". The node will be ignored."); 1797 } 1798 } 1799 } 1800 return result; 1801 } 1802 1803 /** 1804 * Get all context with a permission for the profile for a user 1805 * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query 1806 * @param profileId the profile id 1807 * @return a map of the user's permission for each context 1808 */ 1809 public static Map<Object, Map<UserIdentity, Set<UserOrGroup>>> getProfilePermissionsForUsers(Set< ? extends Object> rootNodes, String profileId) 1810 { 1811 Expression expr = new OrExpression( 1812 new AllowedProfileExpression(profileId), 1813 new DeniedProfileExpression(profileId) 1814 ); 1815 1816 Map<Object, Map<UserIdentity, Set<UserOrGroup>>> result = new HashMap<>(); 1817 for (String rootPath : _convertNodeToPath(rootNodes)) 1818 { 1819 NodeIterator nodes = getACLUsers(rootPath, expr); 1820 1821 while (nodes.hasNext()) 1822 { 1823 Node userNode = nodes.nextNode(); 1824 try 1825 { 1826 String login = ISO9075.decode(userNode.getName()); 1827 String populationId = userNode.getParent().getName(); 1828 1829 UserIdentity user = new UserIdentity(login, populationId); 1830 1831 // Determine the user permissions 1832 Set<UserOrGroup> permission = new HashSet<>(); 1833 1834 if (_getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES).contains(profileId)) 1835 { 1836 permission.add(UserOrGroup.DENIED); 1837 } 1838 else if (_getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES).contains(profileId)) 1839 { 1840 permission.add(UserOrGroup.ALLOWED); 1841 } 1842 1843 // Only add actual permissions to the result 1844 if (!permission.isEmpty()) 1845 { 1846 AmetysObject ao = _getAmetysObjectFromACLNode(userNode); 1847 // The ametys object could already be in the result map having permissions from an other user 1848 Map<UserIdentity, Set<UserOrGroup>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>()); 1849 // There can only be one node per user, so we don't need to retrieve existing value 1850 objectPermissions.put(user, permission); 1851 } 1852 } 1853 catch (RepositoryException e) 1854 { 1855 _logger.error("Failed to retrieve object for group acl node " + userNode.toString() + ". The node will be ignored."); 1856 } 1857 } 1858 } 1859 return result; 1860 } 1861}