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.orgunit.OrgUnit; 053import org.ametys.odf.program.ContainerFactory; 054import org.ametys.odf.program.ProgramFactory; 055import org.ametys.odf.program.ProgramPart; 056import org.ametys.odf.program.SubProgramFactory; 057import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 058import org.ametys.plugins.repository.AmetysObject; 059import org.ametys.plugins.repository.AmetysObjectIterable; 060import org.ametys.plugins.repository.AmetysObjectResolver; 061import org.ametys.plugins.repository.RepositoryConstants; 062import org.ametys.plugins.repository.collection.AmetysObjectCollection; 063import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 064import org.ametys.plugins.repository.query.expression.AndExpression; 065import org.ametys.plugins.repository.query.expression.Expression; 066import org.ametys.runtime.config.Config; 067import org.ametys.runtime.i18n.I18nizableText; 068import org.ametys.runtime.plugin.component.AbstractLogEnabled; 069 070/** 071 * Helper for ODF right 072 * 073 */ 074public class ODFRightHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, Initializable 075{ 076 /** The avalon role */ 077 public static final String ROLE = ODFRightHelper.class.getName(); 078 079 /** Attribute path for contributor role */ 080 public static final String CONTRIBUTORS_FIELD_PATH = "odf-contributors"; 081 /** Attribute path for manager role */ 082 public static final String MANAGERS_FIELD_PATH = "odf-managers"; 083 084 private static final String __PARENTS_CACHE_ID = ODFRightHelper.class.getName() + "$parentsCache"; 085 086 087 /** The ODF helper */ 088 protected ODFHelper _odfHelper; 089 /** The avalon context */ 090 protected Context _context; 091 /** Ametys Object Resolver */ 092 protected AmetysObjectResolver _resolver; 093 /** The cache manager */ 094 protected AbstractCacheManager _cacheManager; 095 096 public void contextualize(Context context) throws ContextException 097 { 098 _context = context; 099 } 100 101 public void service(ServiceManager smanager) throws ServiceException 102 { 103 _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE); 104 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 105 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 106 } 107 108 @Override 109 public void initialize() throws Exception 110 { 111 if (!_cacheManager.hasCache(__PARENTS_CACHE_ID)) 112 { 113 _cacheManager.createRequestCache(__PARENTS_CACHE_ID, 114 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_RIGHTS_PARENT_ELEMENTS_LABEL"), 115 new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_RIGHTS_PARENT_ELEMENTS_DESCRIPTION"), 116 false 117 ); 118 } 119 } 120 121 /** 122 * Get the id of profile for contributors 123 * @return the id of profile for contributors 124 */ 125 public String getContributorProfileId() 126 { 127 return Config.getInstance().getValue("odf.profile.contributor"); 128 } 129 130 /** 131 * Get the id of profile for managers 132 * @return the id of profile for managers 133 */ 134 public String getManagerProfileId() 135 { 136 return Config.getInstance().getValue("odf.profile.manager"); 137 } 138 139 /** 140 * Get the contributors of a {@link ProgramItem} or a {@link OrgUnit} 141 * @param content the program item or the orgunit 142 * @return the contributors or null if not found 143 */ 144 public UserIdentity[] getContributors(Content content) 145 { 146 if (content instanceof OrgUnit || content instanceof ProgramItem) 147 { 148 return content.getValue(CONTRIBUTORS_FIELD_PATH); 149 } 150 151 return null; 152 } 153 154 /** 155 * Build a user query on contributors field 156 * @param users The users to test. 157 * @return the user query 158 */ 159 public Query getContributorsQuery(UserIdentity... users) 160 { 161 return new UsersQuery(CONTRIBUTORS_FIELD_PATH + "_s", Operator.EQ, users); 162 } 163 164 /** 165 * Get the managers of a {@link ProgramItem} or a {@link OrgUnit} 166 * @param content the program item or the orgunit 167 * @return the managers or null if not found 168 */ 169 public UserIdentity[] getManagers(Content content) 170 { 171 if (content instanceof OrgUnit || content instanceof ProgramItem) 172 { 173 return content.getValue(MANAGERS_FIELD_PATH); 174 } 175 176 return null; 177 } 178 179 /** 180 * Build a user query on managers field 181 * @param users The users to test. 182 * @return the user query 183 */ 184 public Query getManagersQuery(UserIdentity... users) 185 { 186 return new UsersQuery(MANAGERS_FIELD_PATH + "_s", Operator.EQ, users); 187 } 188 189 /** 190 * Get the query to search for contents for which the user has a role 191 * @param user the user 192 * @return the query to filter on user permission 193 */ 194 public Query getRolesQuery(UserIdentity user) 195 { 196 List<Query> userQueries = new ArrayList<>(); 197 198 userQueries.add(getContributorsQuery(user)); 199 userQueries.add(getManagersQuery(user)); 200 201 return new OrQuery(userQueries); 202 } 203 204 /** 205 * Get the programs items the user is set with a role 206 * @param user The user 207 * @param roleAttributePath the attribute path for role 208 * @return the program items the user is set with a role 209 */ 210 public AmetysObjectIterable<ProgramItem> getProgramItemsWithUserAsRole(UserIdentity user, String roleAttributePath) 211 { 212 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); 213 Expression roleExpr = new RoleExpression(roleAttributePath, user); 214 215 String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(cTypeExpr, roleExpr)); 216 try (AmetysObjectIterable<ProgramItem> programItems = _resolver.query(query)) 217 { 218 return programItems; 219 } 220 } 221 222 /** 223 * Get the parents of a ODF content from a rights perspective 224 * @param content the content 225 * @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} 226 * @return the parents of the content for rights computing 227 */ 228 public Set<AmetysObject> getParents(Content content, PermissionContext permissionCtx) 229 { 230 Cache<CacheKey, Set<AmetysObject>> cache = _getParentsCache(); 231 232 CacheKey key = CacheKey.of(content.getId(), permissionCtx); 233 return cache.get(key, id -> computeParents(content, permissionCtx)); 234 } 235 /** 236 * Compute the parents of a ODF content from a rights perspective 237 * @param content the content 238 * @param permissionCtx The permission context to compute rights. 239 * @return the parents of the content for rights computing 240 */ 241 protected Set<AmetysObject> computeParents(Content content, PermissionContext permissionCtx) 242 { 243 Set<AmetysObject> parents = new HashSet<>(); 244 245 if (content instanceof ProgramItem programItem) 246 { 247 if (permissionCtx.getAncestor() != null && programItem.equals(permissionCtx.getAncestor())) 248 { 249 // reset ancestor 250 permissionCtx.withAncestor(null); 251 } 252 parents.addAll(computeParentProgramItem(programItem, permissionCtx)); 253 parents.addAll(computeOrgUnits(programItem, permissionCtx)); 254 } 255 else if (content instanceof OrgUnit orgUnit) 256 { 257 OrgUnit parentOrgUnit = orgUnit.getParentOrgUnit(); 258 if (parentOrgUnit != null) 259 { 260 parents.add(parentOrgUnit); 261 } 262 } 263 else if (content instanceof CoursePart coursePart) 264 { 265 List<Course> parentCourses = coursePart.getCourses(); 266 if (!parentCourses.isEmpty()) 267 { 268 parents.addAll(parentCourses); 269 } 270 } 271 272 // default 273 AmetysObject parent = content.getParent(); 274 boolean parentAdded = false; 275 if (parent instanceof AmetysObjectCollection collection && (RepositoryConstants.NAMESPACE_PREFIX + ":contents").equals(collection.getName())) 276 { 277 Request request = ContextHelper.getRequest(_context); 278 String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 279 if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace)) 280 { 281 try 282 { 283 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 284 AmetysObject parentFromDefault = _resolver.resolveByPath(parent.getPath()); 285 parents.add(parentFromDefault); 286 parentAdded = true; 287 } 288 finally 289 { 290 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace); 291 } 292 } 293 } 294 if (!parentAdded) 295 { 296 parents.add(parent); 297 } 298 299 return parents; 300 } 301 302 /** 303 * Get the parents of a program item from a rights perspective 304 * @param programItem the program item 305 * @param permissionCtx The permission context to compute rights. 306 * @return the parent program items for rights computing 307 */ 308 protected List<ProgramItem> computeParentProgramItem(ProgramItem programItem, PermissionContext permissionCtx) 309 { 310 if (programItem instanceof Course course) 311 { 312 return computeParentProgramItem(course, permissionCtx); 313 } 314 else 315 { 316 return _odfHelper.getParentProgramItems(programItem, permissionCtx.getAncestor()); 317 } 318 } 319 320 /** 321 * Get the parents of a course from a rights perspective 322 * @param course the course 323 * @param permissionCtx The permission context to compute rights. 324 * @return the course parents for rights computing 325 */ 326 protected List<ProgramItem> computeParentProgramItem(Course course, PermissionContext permissionCtx) 327 { 328 // Skip course lists to avoid unecessary rights computing on a course list 329 List<ProgramItem> parents = new ArrayList<>(); 330 331 List<CourseList> parentCourseLists = course.getParentCourseLists(); 332 333 boolean ancestorIsParentCL = permissionCtx.getAncestor() != null && parentCourseLists.stream().anyMatch(cl -> cl.equals(permissionCtx.getAncestor())); 334 if (ancestorIsParentCL) 335 { 336 // Required ancestor is a direct parent course list 337 parents.addAll(_odfHelper.getParentProgramItems(permissionCtx.getAncestor(), null)); 338 } 339 else 340 { 341 for (CourseList parentCL : parentCourseLists) 342 { 343 parents.addAll(_odfHelper.getParentProgramItems(parentCL, permissionCtx.getAncestor())); 344 } 345 } 346 347 return parents; 348 } 349 350 /** 351 * Get the orgunits of a program item 352 * @param programItem the program item 353 * @param permissionCtx The permission context to compute rights. 354 * @return the orgunits 355 */ 356 protected List<OrgUnit> computeOrgUnits(ProgramItem programItem, PermissionContext permissionCtx) 357 { 358 Set<String> ouIds = new HashSet<>(); 359 360 if (programItem instanceof CourseList courseList) 361 { 362 List<Course> parentCourses = courseList.getParentCourses(); 363 for (Course parentCourse : parentCourses) 364 { 365 ouIds.addAll(parentCourse.getOrgUnits()); 366 } 367 } 368 else 369 { 370 ouIds.addAll(programItem.getOrgUnits()); 371 } 372 373 return ouIds.stream() 374 .filter(Objects::nonNull) 375 .map(_resolver::resolveById) 376 .map(OrgUnit.class::cast) 377 .toList(); 378 } 379 380 private Cache<CacheKey, Set<AmetysObject>> _getParentsCache() 381 { 382 return _cacheManager.get(__PARENTS_CACHE_ID); 383 } 384 385 static class CacheKey extends AbstractCacheKey 386 { 387 CacheKey(String contentId, PermissionContext permissionCtx) 388 { 389 super(contentId, permissionCtx); 390 } 391 392 static CacheKey of(String contentId, PermissionContext permissionCtx) 393 { 394 return new CacheKey(contentId, permissionCtx); 395 } 396 } 397 398 /** 399 * Class representing the permission context for parents computation. 400 * 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. 401 */ 402 public static class PermissionContext 403 { 404 private Content _initialContent; 405 private ProgramPart _ancestor; 406 407 /** 408 * Constructor 409 * @param initialContent the initail content 410 */ 411 public PermissionContext(Content initialContent) 412 { 413 this(initialContent, null); 414 } 415 416 /** 417 * Constructor 418 * @param initialContent the initail content 419 * @param ancestor The ancestor. Can be null. 420 */ 421 public PermissionContext(Content initialContent, ProgramPart ancestor) 422 { 423 _initialContent = initialContent; 424 _ancestor = ancestor; 425 } 426 427 /** 428 * Get the initial content 429 * @return the initial content 430 */ 431 public Content getInitialContent() 432 { 433 return _initialContent; 434 } 435 436 /** 437 * Set an ancestor. Only parents that part of this ancestor will be returned. 438 * @param ancestor the ancestor 439 */ 440 public void withAncestor(ProgramPart ancestor) 441 { 442 _ancestor = ancestor; 443 } 444 445 /** 446 * Get the ancestor 447 * @return the ancestor. Can be null. 448 */ 449 public ProgramPart getAncestor() 450 { 451 return _ancestor; 452 } 453 } 454 455 /** 456 * Expression for ODF role expression 457 * 458 */ 459 public class RoleExpression implements Expression 460 { 461 private String _attributePath; 462 private UserIdentity _user; 463 464 /** 465 * Constructor 466 * @param attributePath the path of the role attribute 467 * @param user the user 468 */ 469 public RoleExpression(String attributePath, UserIdentity user) 470 { 471 _attributePath = attributePath; 472 _user = user; 473 } 474 475 public String build() 476 { 477 StringBuilder buff = new StringBuilder(); 478 479 buff.append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append(_attributePath).append("/*/").append('@').append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append("login"); 480 buff.append(" " + Operator.EQ); 481 buff.append(" '" + _user.getLogin() + "'"); 482 483 buff.append(LogicalOperator.AND.toString()); 484 485 buff.append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append(_attributePath).append("/*/").append('@').append(RepositoryConstants.NAMESPACE_PREFIX).append(':').append("population"); 486 buff.append(" " + Operator.EQ); 487 buff.append(" '" + _user.getPopulationId() + "'"); 488 489 return buff.toString(); 490 } 491 } 492}