001/* 002 * Copyright 2023 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.odf.rights; 017 018import java.util.ArrayList; 019import java.util.HashSet; 020import java.util.List; 021import java.util.Objects; 022import java.util.Set; 023 024import org.apache.avalon.framework.activity.Initializable; 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.context.Context; 027import org.apache.avalon.framework.context.ContextException; 028import org.apache.avalon.framework.context.Contextualizable; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.components.ContextHelper; 033import org.apache.cocoon.environment.Request; 034 035import org.ametys.cms.content.archive.ArchiveConstants; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.ContentQueryHelper; 038import org.ametys.cms.repository.ContentTypeExpression; 039import org.ametys.cms.search.query.OrQuery; 040import org.ametys.cms.search.query.Query; 041import org.ametys.cms.search.query.Query.Operator; 042import org.ametys.cms.search.query.UsersQuery; 043import org.ametys.core.cache.AbstractCacheManager; 044import org.ametys.core.cache.Cache; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.odf.ODFHelper; 047import org.ametys.odf.ProgramItem; 048import org.ametys.odf.course.Course; 049import org.ametys.odf.course.CourseFactory; 050import org.ametys.odf.courselist.CourseList; 051import org.ametys.odf.coursepart.CoursePart; 052import org.ametys.odf.data.EducationalPath; 053import org.ametys.odf.orgunit.OrgUnit; 054import org.ametys.odf.orgunit.OrgUnitFactory; 055import org.ametys.odf.program.ContainerFactory; 056import org.ametys.odf.program.ProgramFactory; 057import org.ametys.odf.program.ProgramPart; 058import org.ametys.odf.program.SubProgramFactory; 059import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 060import org.ametys.plugins.repository.AmetysObject; 061import org.ametys.plugins.repository.AmetysObjectIterable; 062import org.ametys.plugins.repository.AmetysObjectResolver; 063import org.ametys.plugins.repository.RepositoryConstants; 064import org.ametys.plugins.repository.UnknownAmetysObjectException; 065import org.ametys.plugins.repository.collection.AmetysObjectCollection; 066import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 067import org.ametys.plugins.repository.query.expression.AndExpression; 068import org.ametys.plugins.repository.query.expression.Expression; 069import org.ametys.runtime.config.Config; 070import org.ametys.runtime.i18n.I18nizableText; 071import org.ametys.runtime.plugin.component.AbstractLogEnabled; 072 073/** 074 * Helper for ODF right 075 */ 076public class ODFRightHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, Initializable 077{ 078 /** The avalon role */ 079 public static final String ROLE = ODFRightHelper.class.getName(); 080 081 /** Request attribute name for storing current educational paths */ 082 public static final String REQUEST_ATTR_EDUCATIONAL_PATHS = ODFRightHelper.class.getName() + "$educationalPath"; 083 084 /** Attribute path for contributor role */ 085 public static final String CONTRIBUTORS_FIELD_PATH = "odf-contributors"; 086 /** Attribute path for manager role */ 087 public static final String MANAGERS_FIELD_PATH = "odf-managers"; 088 089 private static final String __PARENTS_CACHE_ID = ODFRightHelper.class.getName() + "$parentsCache"; 090 091 /** The ODF helper */ 092 protected ODFHelper _odfHelper; 093 /** The avalon context */ 094 protected Context _context; 095 /** Ametys Object Resolver */ 096 protected AmetysObjectResolver _resolver; 097 /** The cache manager */ 098 protected AbstractCacheManager _cacheManager; 099 100 public void contextualize(Context context) throws ContextException 101 { 102 _context = context; 103 } 104 105 public void service(ServiceManager smanager) throws ServiceException 106 { 107 _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE); 108 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 109 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 110 } 111 112 @Override 113 public void initialize() throws Exception 114 { 115 if (!_cacheManager.hasCache(__PARENTS_CACHE_ID)) 116 { 117 _cacheManager.createRequestCache(__PARENTS_CACHE_ID, 118 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_RIGHTS_PARENT_ELEMENTS_LABEL"), 119 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_RIGHTS_PARENT_ELEMENTS_DESCRIPTION"), 120 false 121 ); 122 } 123 } 124 125 /** 126 * Get the id of profile for contributors 127 * @return the id of profile for contributors 128 */ 129 public String getContributorProfileId() 130 { 131 return Config.getInstance().getValue("odf.profile.contributor"); 132 } 133 134 /** 135 * Get the id of profile for managers 136 * @return the id of profile for managers 137 */ 138 public String getManagerProfileId() 139 { 140 return Config.getInstance().getValue("odf.profile.manager"); 141 } 142 143 /** 144 * Get the contributors of a {@link ProgramItem} or a {@link OrgUnit} 145 * @param content the program item or the orgunit 146 * @return the contributors or null if not found 147 */ 148 public UserIdentity[] getContributors(Content content) 149 { 150 if (content instanceof OrgUnit || content instanceof ProgramItem) 151 { 152 return content.getValue(CONTRIBUTORS_FIELD_PATH); 153 } 154 155 return null; 156 } 157 158 /** 159 * Build a user query on contributors field 160 * @param users The users to test. 161 * @return the user query 162 */ 163 public Query getContributorsQuery(UserIdentity... users) 164 { 165 return new UsersQuery(CONTRIBUTORS_FIELD_PATH + "_s", Operator.EQ, users); 166 } 167 168 /** 169 * Get the managers of a {@link ProgramItem} or a {@link OrgUnit} 170 * @param content the program item or the orgunit 171 * @return the managers or null if not found 172 */ 173 public UserIdentity[] getManagers(Content content) 174 { 175 if (content instanceof OrgUnit || content instanceof ProgramItem) 176 { 177 return content.getValue(MANAGERS_FIELD_PATH); 178 } 179 180 return null; 181 } 182 183 /** 184 * Build a user query on managers field 185 * @param users The users to test. 186 * @return the user query 187 */ 188 public Query getManagersQuery(UserIdentity... users) 189 { 190 return new UsersQuery(MANAGERS_FIELD_PATH + "_s", Operator.EQ, users); 191 } 192 193 /** 194 * Get the query to search for contents for which the user has a role 195 * @param user the user 196 * @return the query to filter on user permission 197 */ 198 public Query getRolesQuery(UserIdentity user) 199 { 200 List<Query> userQueries = new ArrayList<>(); 201 202 userQueries.add(getContributorsQuery(user)); 203 userQueries.add(getManagersQuery(user)); 204 205 return new OrQuery(userQueries); 206 } 207 208 /** 209 * Get the ODF contents with users with given role 210 * @param roleAttributePath the attribute path for role 211 * @return the contents with user(s) set with a role 212 */ 213 public AmetysObjectIterable<Content> getContentsWithRole(String roleAttributePath) 214 { 215 Expression cTypeExpr = new ContentTypeExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE, SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, ContainerFactory.CONTAINER_CONTENT_TYPE, CourseFactory.COURSE_CONTENT_TYPE, OrgUnitFactory.ORGUNIT_CONTENT_TYPE); 216 Expression roleExpr = new RoleExpression(roleAttributePath); 217 218 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, roleExpr)); 219 try (AmetysObjectIterable<Content> contents = _resolver.query(query)) 220 { 221 return contents; 222 } 223 } 224 225 /** 226 * Get the programs items the user is set with a role 227 * @param user The user 228 * @param roleAttributePath the attribute path for role 229 * @return the program items the user is set with a role 230 */ 231 public AmetysObjectIterable<Content> getContentsWithUserAsRole(UserIdentity user, String roleAttributePath) 232 { 233 Expression cTypeExpr = new ContentTypeExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE, SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, ContainerFactory.CONTAINER_CONTENT_TYPE, CourseFactory.COURSE_CONTENT_TYPE, OrgUnitFactory.ORGUNIT_CONTENT_TYPE); 234 Expression roleExpr = new RoleExpression(roleAttributePath, user); 235 236 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, roleExpr)); 237 try (AmetysObjectIterable<Content> contents = _resolver.query(query)) 238 { 239 return contents; 240 } 241 } 242 243 /** 244 * Get the parents of a ODF content from a rights perspective 245 * @param content the content 246 * @param permissionCtx The permission context to compute rights. The permission context allows to restrict parents to parents which are part of a given ancestor. See #withAncestor method of {@link PermissionContext} 247 * @return the parents of the content for rights computing 248 */ 249 public Set<AmetysObject> getParents(Content content, PermissionContext permissionCtx) 250 { 251 Cache<CacheKey, Set<AmetysObject>> cache = _getParentsCache(); 252 253 CacheKey key = CacheKey.of(content.getId(), permissionCtx); 254 return cache.get(key, id -> computeParents(content, permissionCtx)); 255 } 256 /** 257 * Compute the parents of a ODF content from a rights perspective 258 * @param content the content 259 * @param permissionCtx The permission context to compute rights. 260 * @return the parents of the content for rights computing 261 */ 262 protected Set<AmetysObject> computeParents(Content content, PermissionContext permissionCtx) 263 { 264 Set<AmetysObject> parents = new HashSet<>(); 265 266 if (content instanceof ProgramItem programItem) 267 { 268 if (permissionCtx.getAncestor() != null && programItem.equals(permissionCtx.getAncestor())) 269 { 270 // reset ancestor 271 permissionCtx.withAncestor(null); 272 } 273 parents.addAll(computeParentProgramItem(programItem, permissionCtx)); 274 parents.addAll(computeOrgUnits(programItem, permissionCtx)); 275 } 276 else if (content instanceof OrgUnit orgUnit) 277 { 278 OrgUnit parentOrgUnit = orgUnit.getParentOrgUnit(); 279 if (parentOrgUnit != null) 280 { 281 parents.add(parentOrgUnit); 282 } 283 } 284 else if (content instanceof CoursePart coursePart) 285 { 286 List<Course> parentCourses = coursePart.getCourses(); 287 if (!parentCourses.isEmpty()) 288 { 289 parents.addAll(parentCourses); 290 } 291 } 292 293 // default 294 AmetysObject parent = content.getParent(); 295 boolean parentAdded = false; 296 if (parent instanceof AmetysObjectCollection collection && (RepositoryConstants.NAMESPACE_PREFIX + ":contents").equals(collection.getName())) 297 { 298 Request request = ContextHelper.getRequest(_context); 299 String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 300 if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace)) 301 { 302 try 303 { 304 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 305 AmetysObject parentFromDefault = _resolver.resolveByPath(parent.getPath()); 306 parents.add(parentFromDefault); 307 parentAdded = true; 308 } 309 finally 310 { 311 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace); 312 } 313 } 314 } 315 if (!parentAdded) 316 { 317 parents.add(parent); 318 } 319 320 return parents; 321 } 322 323 /** 324 * Get the parents of a program item from a rights perspective 325 * @param programItem the program item 326 * @param permissionCtx The permission context to compute rights. 327 * @return the parent program items for rights computing 328 */ 329 protected List<ProgramItem> computeParentProgramItem(ProgramItem programItem, PermissionContext permissionCtx) 330 { 331 if (programItem instanceof Course course) 332 { 333 return computeParentProgramItem(course, permissionCtx); 334 } 335 else 336 { 337 return _odfHelper.getParentProgramItems(programItem, permissionCtx.getAncestor()); 338 } 339 } 340 341 /** 342 * Get the parents of a course from a rights perspective 343 * @param course the course 344 * @param permissionCtx The permission context to compute rights. 345 * @return the course parents for rights computing 346 */ 347 protected List<ProgramItem> computeParentProgramItem(Course course, PermissionContext permissionCtx) 348 { 349 // Skip course lists to avoid unecessary rights computing on a course list 350 List<ProgramItem> parents = new ArrayList<>(); 351 352 List<CourseList> parentCourseLists = course.getParentCourseLists(); 353 354 boolean ancestorIsParentCL = permissionCtx.getAncestor() != null && parentCourseLists.stream().anyMatch(cl -> cl.equals(permissionCtx.getAncestor())); 355 if (ancestorIsParentCL) 356 { 357 // Required ancestor is a direct parent course list 358 parents.addAll(_odfHelper.getParentProgramItems(permissionCtx.getAncestor(), null)); 359 } 360 else 361 { 362 for (CourseList parentCL : parentCourseLists) 363 { 364 parents.addAll(_odfHelper.getParentProgramItems(parentCL, permissionCtx.getAncestor())); 365 } 366 } 367 368 return parents; 369 } 370 371 /** 372 * Get the orgunits of a program item 373 * @param programItem the program item 374 * @param permissionCtx The permission context to compute rights. 375 * @return the orgunits 376 */ 377 protected List<OrgUnit> computeOrgUnits(ProgramItem programItem, PermissionContext permissionCtx) 378 { 379 Set<String> ouIds = new HashSet<>(); 380 381 if (programItem instanceof CourseList courseList) 382 { 383 List<Course> parentCourses = courseList.getParentCourses(); 384 for (Course parentCourse : parentCourses) 385 { 386 ouIds.addAll(parentCourse.getOrgUnits()); 387 } 388 } 389 else 390 { 391 ouIds.addAll(programItem.getOrgUnits()); 392 } 393 394 return ouIds.stream() 395 .filter(Objects::nonNull) 396 .map(this::_resolveSilently) 397 .filter(Objects::nonNull) 398 .toList(); 399 } 400 401 private OrgUnit _resolveSilently(String orgUnitId) 402 { 403 try 404 { 405 return _resolver.resolveById(orgUnitId); 406 } 407 catch (UnknownAmetysObjectException e) 408 { 409 if (getLogger().isDebugEnabled()) 410 { 411 getLogger().debug("The orgunit with id {} does not exist, it will be ignored in rights computing", orgUnitId); 412 } 413 return null; 414 } 415 } 416 417 private Cache<CacheKey, Set<AmetysObject>> _getParentsCache() 418 { 419 return _cacheManager.get(__PARENTS_CACHE_ID); 420 } 421 422 static class CacheKey extends AbstractCacheKey 423 { 424 CacheKey(String contentId, PermissionContext permissionCtx) 425 { 426 super(contentId, permissionCtx); 427 } 428 429 static CacheKey of(String contentId, PermissionContext permissionCtx) 430 { 431 return new CacheKey(contentId, permissionCtx); 432 } 433 } 434 435 /** 436 * Class representing the permission context for parents computation. 437 * The permission context is composed by the initial content and an optional program part ancestor to restrict parents to parents which is part of this ancestor. 438 */ 439 public static class PermissionContext 440 { 441 private Content _initialContent; 442 private ProgramPart _ancestor; 443 444 /** 445 * Constructor 446 * @param initialContent the initail content 447 */ 448 public PermissionContext(Content initialContent) 449 { 450 this(initialContent, null); 451 } 452 453 /** 454 * Constructor 455 * @param initialContent the initail content 456 * @param ancestor The ancestor. Can be null. 457 */ 458 public PermissionContext(Content initialContent, ProgramPart ancestor) 459 { 460 _initialContent = initialContent; 461 _ancestor = ancestor; 462 } 463 464 /** 465 * Get the initial content 466 * @return the initial content 467 */ 468 public Content getInitialContent() 469 { 470 return _initialContent; 471 } 472 473 /** 474 * Set an ancestor. Only parents that part of this ancestor will be returned. 475 * @param ancestor the ancestor 476 */ 477 public void withAncestor(ProgramPart ancestor) 478 { 479 _ancestor = ancestor; 480 } 481 482 /** 483 * Get the ancestor 484 * @return the ancestor. Can be null. 485 */ 486 public ProgramPart getAncestor() 487 { 488 return _ancestor; 489 } 490 } 491 492 /** 493 * Class representing the permission context for parents computation for a contextualized content. 494 * The permission context is composed by the initial content and a {@link EducationalPath} 495 */ 496 public static class ContextualizedPermissionContext 497 { 498 private Content _initialContent; 499 private EducationalPath _educationalPath; 500 501 /** 502 * Constructor 503 * @param initialContent the initial content 504 */ 505 public ContextualizedPermissionContext(Content initialContent) 506 { 507 this(initialContent, null); 508 } 509 510 /** 511 * Constructor 512 * @param initialContent the initial program item 513 * @param educationalPath The educational path. Can be null. 514 */ 515 public ContextualizedPermissionContext(Content initialContent, EducationalPath educationalPath) 516 { 517 _initialContent = initialContent; 518 _educationalPath = educationalPath; 519 } 520 521 /** 522 * Get the initial content 523 * @return the initial content 524 */ 525 public Content getInitialContent() 526 { 527 return _initialContent; 528 } 529 530 /** 531 * Get the educational path in permission context 532 * @return the educational path 533 */ 534 public EducationalPath getEducationalPath() 535 { 536 return _educationalPath; 537 } 538 539 /** 540 * Set an educational path. Only parent that part of this educational will be returned. 541 * @param educationalPath the educationa path 542 */ 543 public void withEducationalPath(EducationalPath educationalPath) 544 { 545 _educationalPath = educationalPath; 546 } 547 } 548 549 /** 550 * Record representing a contextualized content 551 * @param content the content 552 * @param path the context as a {@link EducationalPath} 553 */ 554 public record ContextualizedContent(Content content, EducationalPath path) { } 555 556 /** 557 * Expression for ODF role expression 558 * 559 */ 560 public class RoleExpression implements Expression 561 { 562 private String _attributePath; 563 private UserIdentity _user; 564 565 /** 566 * Constructor 567 * @param attributePath the path of the role attribute 568 */ 569 public RoleExpression(String attributePath) 570 { 571 this(attributePath, null); 572 } 573 574 /** 575 * Constructor 576 * @param attributePath the path of the role attribute 577 * @param user the user. Can be null. 578 */ 579 public RoleExpression(String attributePath, UserIdentity user) 580 { 581 _attributePath = attributePath; 582 _user = user; 583 } 584 585 public String build() 586 { 587 StringBuilder buff = new StringBuilder(); 588 589 buff.append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append(_attributePath).append("/*/").append('@').append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append("login"); 590 if (_user != null) 591 { 592 buff.append(" " + Operator.EQ); 593 buff.append(" '" + _user.getLogin() + "'"); 594 } 595 596 buff.append(LogicalOperator.AND.toString()); 597 598 buff.append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append(_attributePath).append("/*/").append('@').append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append("population"); 599 if (_user != null) 600 { 601 buff.append(" " + Operator.EQ); 602 buff.append(" '" + _user.getPopulationId() + "'"); 603 } 604 605 return buff.toString(); 606 } 607 } 608}