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.HashMap; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022import java.util.stream.Collectors; 023 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.avalon.framework.service.Serviceable; 027import org.apache.commons.lang.StringUtils; 028 029import org.ametys.cms.repository.Content; 030import org.ametys.core.group.GroupIdentity; 031import org.ametys.core.right.AccessController; 032import org.ametys.core.right.AccessController.Permission.PermissionType; 033import org.ametys.core.right.AccessExplanation; 034import org.ametys.core.right.ProfileBasedAccessController; 035import org.ametys.core.right.RightProfilesDAO; 036import org.ametys.core.right.RightsException; 037import org.ametys.core.user.UserIdentity; 038import org.ametys.odf.ProgramItem; 039import org.ametys.odf.orgunit.OrgUnit; 040import org.ametys.odf.rights.ODFRightHelper.PermissionContext; 041import org.ametys.odf.tree.ODFContentsTreeHelper; 042import org.ametys.plugins.core.impl.right.WorkspaceAccessController; 043import org.ametys.plugins.repository.AmetysObjectIterable; 044import org.ametys.plugins.repository.AmetysObjectResolver; 045import org.ametys.runtime.i18n.I18nizableText; 046import org.ametys.runtime.i18n.I18nizableTextParameter; 047import org.ametys.runtime.plugin.component.PluginAware; 048 049/** 050 * Abstract class for access controller based of a ODF role attribute 051 * 052 */ 053public abstract class AbstractODFRoleAccessController implements ProfileBasedAccessController, Serviceable, PluginAware 054{ 055 private static final String __CMS_RIGHT_CONTEXT = "/cms"; 056 057 /** The rights profile DAO */ 058 protected RightProfilesDAO _rightProfileDAO; 059 /** The ODF contents tree helper */ 060 protected ODFContentsTreeHelper _odfContentsTreeHelper; 061 /** The ODF right helper */ 062 protected ODFRightHelper _odfRightHelper; 063 /** The ametys resolver */ 064 protected AmetysObjectResolver _resolver; 065 /** The role access helper */ 066 protected ODFRoleAccessControllerHelper _roleAccessControllerHelper; 067 068 private String _id; 069 070 071 public void service(ServiceManager smanager) throws ServiceException 072 { 073 _rightProfileDAO = (RightProfilesDAO) smanager.lookup(RightProfilesDAO.ROLE); 074 _odfContentsTreeHelper = (ODFContentsTreeHelper) smanager.lookup(ODFContentsTreeHelper.ROLE); 075 _odfRightHelper = (ODFRightHelper) smanager.lookup(org.ametys.odf.rights.ODFRightHelper.ROLE); 076 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 077 _roleAccessControllerHelper = (ODFRoleAccessControllerHelper) smanager.lookup(ODFRoleAccessControllerHelper.ROLE); 078 } 079 080 public void setPluginInfo(String pluginName, String featureName, String id) 081 { 082 _id = id; 083 } 084 085 public String getId() 086 { 087 return _id; 088 } 089 090 public boolean supports(Object object) 091 { 092 return object instanceof ProgramItem || object instanceof OrgUnit || object instanceof String && ((String) object).startsWith(__CMS_RIGHT_CONTEXT); 093 } 094 095 /** 096 * Get the parents of the content for rights purpose 097 * @param content the content 098 * @param permissionCtx the permission context 099 * @return the parents of content 100 */ 101 protected Set<Content> _getParents(Content content, PermissionContext permissionCtx) 102 { 103 return _odfRightHelper.getParents(content, permissionCtx).stream() 104 .filter(Content.class::isInstance) 105 .map (Content.class::cast) 106 .collect(Collectors.toSet()); 107 } 108 109 /** 110 * Get the permission context 111 * @param initialContent the initial content 112 * @return the permission context. 113 */ 114 protected PermissionContext _getPermissionContext(Content initialContent) 115 { 116 return new PermissionContext(initialContent); 117 } 118 119 public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object) 120 { 121 if (object instanceof String) 122 { 123 if (_roleAccessControllerHelper.hasODFRoleOnAnyContent(user, _getRoleAttributePath())) 124 { 125 // allow user on CMS context if he has permission on at least a program item 126 return _getRightsInTargetProfile().contains(rightId) ? AccessResult.USER_ALLOWED : AccessResult.UNKNOWN; 127 } 128 else 129 { 130 return AccessResult.UNKNOWN; 131 } 132 } 133 else 134 { 135 return _getPermission(user, userGroups, rightId, (Content) object, _getPermissionContext((Content) object)); 136 } 137 } 138 139 private AccessResult _getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Content object, PermissionContext permissionCtx) 140 { 141 List<String> rights = _getRightsInTargetProfile(); 142 if (rights.contains(rightId)) 143 { 144 Set<UserIdentity> allowedUsers = _getLocalAllowedUsers(object); 145 if (allowedUsers.contains(user)) 146 { 147 return AccessResult.USER_ALLOWED; 148 } 149 } 150 151 AccessResult permission = AccessResult.UNKNOWN; 152 153 Set<Content> parents = _getParents(object, permissionCtx); 154 if (parents != null) 155 { 156 for (Content parent : parents) 157 { 158 AccessResult parentResult = _getPermission(user, userGroups, rightId, parent, permissionCtx); 159 permission = AccessResult.merge(permission, parentResult); 160 } 161 } 162 163 return permission; 164 } 165 166 /** 167 * Get the rights hold by target profile 168 * @return the rights hold by target profile 169 */ 170 protected synchronized List<String> _getRightsInTargetProfile() 171 { 172 String profileId = _getTargetProfileId(); 173 return StringUtils.isNotBlank(profileId) ? _rightProfileDAO.getRights(profileId) : List.of(); 174 } 175 176 /** 177 * Get the id of target profile 178 * @return the id of target profile 179 */ 180 protected abstract String _getTargetProfileId(); 181 182 /** 183 * Get the allowed users for this content taking into account the content itself and its parents 184 * @param content the ODF content (program item or orgunit) 185 * @param permissionCtx the permission context 186 * @return the allowed users. Empty if no user is allowed on this content 187 */ 188 protected Set<UserIdentity> _getAllowedUsers(Content content, PermissionContext permissionCtx) 189 { 190 Set<UserIdentity> allowedUsers = _getLocalAllowedUsers(content); 191 192 Set<Content> parents = _getParents(content, permissionCtx); 193 if (parents != null) 194 { 195 for (Content parent : parents) 196 { 197 allowedUsers.addAll(_getAllowedUsers(parent, permissionCtx)); 198 } 199 } 200 201 return allowedUsers; 202 } 203 204 /** 205 * Get the local allowed users for this content 206 * @param content the ODF content (program item or orgunit) 207 * @return the allowed users. Empty if no user is allowed on this content 208 */ 209 protected abstract Set<UserIdentity> _getLocalAllowedUsers(Content content); 210 211 public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object) 212 { 213 return AccessResult.UNKNOWN; 214 } 215 216 public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object) 217 { 218 if (object instanceof String) 219 { 220 if (_roleAccessControllerHelper.hasODFRoleOnAnyContent(user, _getRoleAttributePath())) 221 { 222 return _getRightsInTargetProfile().stream() 223 .collect(Collectors.toMap(r -> r, r -> AccessResult.USER_ALLOWED)); 224 } 225 } 226 else 227 { 228 Set<UserIdentity> allowedUsers = _getAllowedUsers((Content) object, _getPermissionContext((Content) object)); 229 if (allowedUsers.contains(user)) 230 { 231 return _getRightsInTargetProfile().stream() 232 .collect(Collectors.toMap(r -> r, r -> AccessResult.USER_ALLOWED)); 233 } 234 } 235 return Map.of(); 236 } 237 238 /** 239 * Get the attribute path for role 240 * @return the attribute path for role 241 */ 242 protected abstract String _getRoleAttributePath(); 243 244 public AccessResult getPermissionForAnonymous(String rightId, Object object) 245 { 246 return AccessResult.UNKNOWN; 247 } 248 249 public AccessResult getReadAccessPermissionForAnonymous(Object object) 250 { 251 return AccessResult.UNKNOWN; 252 } 253 254 public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object) 255 { 256 return AccessResult.UNKNOWN; 257 } 258 259 public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object) 260 { 261 return AccessResult.UNKNOWN; 262 } 263 264 public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object) 265 { 266 if (object instanceof Content && _getRightsInTargetProfile().contains(rightId)) 267 { 268 Set<UserIdentity> allowedUsers = _getAllowedUsers((Content) object, _getPermissionContext((Content) object)); 269 if (allowedUsers != null) 270 { 271 return allowedUsers.stream() 272 .collect(Collectors.toMap(user -> user, user -> AccessResult.USER_ALLOWED)); 273 } 274 } 275 return Map.of(); 276 } 277 278 public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object) 279 { 280 return Map.of(); 281 } 282 283 public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object) 284 { 285 return Map.of(); 286 } 287 288 public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object) 289 { 290 return Map.of(); 291 } 292 293 public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId) 294 { 295 boolean supportWorkspaceCtx = workspacesContexts.stream() 296 .filter(String.class::isInstance) 297 .map(String.class::cast) 298 .anyMatch(ctx -> ctx.startsWith(__CMS_RIGHT_CONTEXT)); 299 300 if (supportWorkspaceCtx && _roleAccessControllerHelper.hasODFRoleOnAnyContent(user, _getRoleAttributePath())) 301 { 302 // allow BO access if user has permission on at least a program item 303 return _getRightsInTargetProfile().contains(rightId); 304 } 305 return false; 306 } 307 308 public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups) 309 { 310 return false; 311 } 312 313 public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId) 314 { 315 return false; 316 } 317 318 public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts) 319 { 320 return false; 321 } 322 323 public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId) 324 { 325 return false; 326 } 327 328 public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts) 329 { 330 return false; 331 } 332 333 @Override 334 public AccessExplanation explainPermission(UserIdentity user, Set<GroupIdentity> groups, String rightId, Object object) 335 { 336 if (_getRightsInTargetProfile().contains(rightId)) 337 { 338 return _explainPermissionForRole(user, object, true); 339 } 340 else 341 { 342 return AccessController.getDefaultAccessExplanation(getId(), AccessResult.UNKNOWN); 343 } 344 } 345 346 private AccessExplanation _explainPermissionForRole(UserIdentity user, Object object, boolean withHierarchy) 347 { 348 if (object instanceof String) 349 { 350 if (_roleAccessControllerHelper.hasODFRoleOnAnyContent(user, _getRoleAttributePath())) 351 { 352 // allow user on CMS context if he has permission on at least a program item 353 return new AccessExplanation(getId(), AccessResult.USER_ALLOWED, 354 new I18nizableText("plugin.odf", "PLUGINS_ODF_ROLE_ACCESS_CONTROLLER_GENERAL_EXPLANATION", 355 Map.of("role", _getRoleLabel()))); 356 } 357 else 358 { 359 return AccessController.getDefaultAccessExplanation(getId(), AccessResult.UNKNOWN); 360 } 361 } 362 else 363 { 364 PermissionDetails details = _getPermissionDetails(user, (Content) object, _getPermissionContext((Content) object), withHierarchy); 365 return _buildExplanation(details); 366 } 367 } 368 369 private PermissionDetails _getPermissionDetails(UserIdentity user, Content content, PermissionContext permissionCtx, boolean withHierarchy) 370 { 371 Set<UserIdentity> allowedUsers = _getLocalAllowedUsers(content); 372 if (allowedUsers.contains(user)) 373 { 374 return new PermissionDetails(AccessResult.USER_ALLOWED, content, false); 375 } 376 377 PermissionDetails details = new PermissionDetails(AccessResult.UNKNOWN, content, false); 378 if (withHierarchy) 379 { 380 Set<Content> parents = _getParents(content, permissionCtx); 381 if (parents != null) 382 { 383 for (Content parent : parents) 384 { 385 PermissionDetails parentDetails = _getPermissionDetails(user, parent, permissionCtx, true); 386 387 AccessResult parentResult = parentDetails.result(); 388 if (parentResult != AccessResult.UNKNOWN && AccessResult.merge(parentResult, details.result()) == parentResult) 389 { 390 // FIXME here we arbitrarily keep the last explanation but we should merge instead 391 // Build a new explanation only if the actual not inherited 392 details = parentDetails.inherited() ? parentDetails : new PermissionDetails(parentResult, parentDetails.object(), true); 393 } 394 } 395 } 396 } 397 398 return details; 399 } 400 401 private AccessExplanation _buildExplanation(PermissionDetails details) 402 { 403 AccessResult result = details.result(); 404 if (AccessResult.UNKNOWN.equals(result)) 405 { 406 return AccessController.getDefaultAccessExplanation(getId(), AccessResult.UNKNOWN); 407 } 408 409 Map<String, I18nizableTextParameter> params = Map.of( 410 "title", new I18nizableText(details.object().getTitle()), 411 "role", _getRoleLabel() 412 ); 413 I18nizableText label; 414 if (details.inherited()) 415 { 416 label = new I18nizableText("plugin.odf", "PLUGINS_ODF_ROLE_ACCESS_CONTROLLER_INHERITED_EXPLANATION", params); 417 } 418 else 419 { 420 label = new I18nizableText("plugin.odf", "PLUGINS_ODF_ROLE_ACCESS_CONTROLLER_EXPLANATION", params); 421 } 422 return new AccessExplanation(getId(), details.result(), label); 423 } 424 425 /** 426 * Get the label to insert in the explanation to describe the role. 427 * The label should start with a lower case. 428 * @return the label 429 */ 430 protected abstract I18nizableText _getRoleLabel(); 431 432 public Map<ExplanationObject, Map<Permission, AccessExplanation>> explainAllPermissions(UserIdentity identity, Set<GroupIdentity> groups, Set<Object> workspacesContexts) 433 { 434 Map<ExplanationObject, Map<Permission, AccessExplanation>> result = new HashMap<>(); 435 436 if (workspacesContexts.contains(__CMS_RIGHT_CONTEXT)) 437 { 438 try (AmetysObjectIterable<Content> contentsWithUserAsRole = _odfRightHelper.getContentsWithUserAsRole(identity, _getRoleAttributePath())) 439 { 440 for (Content content : contentsWithUserAsRole) 441 { 442 AccessExplanation explanation = _explainPermissionForRole(identity, content, false); 443 if (explanation.accessResult() != AccessResult.UNKNOWN) 444 { 445 Map<Permission, AccessExplanation> contextPermission = new HashMap<>(); 446 contextPermission.put(new Permission(PermissionType.PROFILE, _getTargetProfileId()), explanation); 447 448 result.put(getExplanationObject(content), contextPermission); 449 } 450 } 451 } 452 453 String generalContext = __CMS_RIGHT_CONTEXT; 454 AccessExplanation explanation = _explainPermissionForRole(identity, generalContext, false); 455 if (explanation.accessResult() != AccessResult.UNKNOWN) 456 { 457 Map<Permission, AccessExplanation> contextPermission = new HashMap<>(); 458 contextPermission.put(new Permission(PermissionType.PROFILE, _getTargetProfileId()), explanation); 459 460 result.put(getExplanationObject(generalContext), contextPermission); 461 } 462 } 463 464 return result; 465 } 466 467 public I18nizableText getObjectLabel(Object object) 468 { 469 if (object instanceof String) 470 { 471 return WorkspaceAccessController.GENERAL_CONTEXT_CATEGORY; 472 } 473 else if (object instanceof Content content) 474 { 475 return ODFContentHierarchicalAccessController.getContentObjectLabel(content, _odfContentsTreeHelper); 476 } 477 throw new RightsException("Unsupported object: " + object.toString()); 478 } 479 480 public I18nizableText getObjectCategory(Object object) 481 { 482 if (object instanceof String) 483 { 484 return WorkspaceAccessController.GENERAL_CONTEXT_CATEGORY; 485 } 486 else 487 { 488 return ODFContentHierarchicalAccessController.ODF_CONTEXT_CATEGORY; 489 } 490 } 491 492 public Map<Permission, AccessExplanation> explainAllPermissionsForAnonymous(Object object) 493 { 494 return Map.of(); 495 } 496 497 public Map<Permission, AccessExplanation> explainAllPermissionsForAnyConnected(Object object) 498 { 499 return Map.of(); 500 } 501 502 public Map<UserIdentity, Map<Permission, AccessExplanation>> explainAllPermissionsByUser(Object object) 503 { 504 if (object instanceof String) 505 { 506 // TODO find a way to list every user with a role 507 return Map.of(); 508 } 509 else 510 { 511 Map<UserIdentity, Map<Permission, AccessExplanation>> results = new HashMap<>(); 512 Permission permission = new Permission(PermissionType.PROFILE, _getTargetProfileId()); 513 514 for (UserIdentity user: _getAllowedUsers((Content) object, _getPermissionContext((Content) object))) 515 { 516 PermissionDetails details = _getPermissionDetails(user, (Content) object, _getPermissionContext((Content) object), true); 517 AccessExplanation explanation = _buildExplanation(details); 518 results.put(user, Map.of(permission, explanation)); 519 } 520 521 return results; 522 } 523 } 524 525 public Map<GroupIdentity, Map<Permission, AccessExplanation>> explainAllPermissionsByGroup(Object object) 526 { 527 return Map.of(); 528 } 529 530 public Map<ExplanationObject, AccessExplanation> explainAllProfileUsesForAnonymousOnWorkspaces(String profileId, Set<Object> workspacesContexts) 531 { 532 return Map.of(); 533 } 534 535 public Map<ExplanationObject, AccessExplanation> explainAllProfileUsesForAnyConnectedOnWorkspaces(String profileId, Set<Object> workspacesContexts) 536 { 537 return Map.of(); 538 } 539 540 public Map<ExplanationObject, Map<GroupIdentity, AccessExplanation>> explainAllProfileUsesOnWorkspacesByGroups(String profileId, Set<Object> workspacesContexts) 541 { 542 return Map.of(); 543 } 544 545 public Map<ExplanationObject, Map<UserIdentity, AccessExplanation>> explainAllProfileUsesOnWorkspacesByUser(String profileId, Set<Object> workspacesContexts) 546 { 547 if (workspacesContexts.contains(__CMS_RIGHT_CONTEXT) && profileId.equals(_getTargetProfileId())) 548 { 549 Map<ExplanationObject, Map<UserIdentity, AccessExplanation>> result = new HashMap<>(); 550 551 try (AmetysObjectIterable<Content> contents = _odfRightHelper.getContentsWithRole(_getRoleAttributePath())) 552 { 553 for (Content content: contents) 554 { 555 UserIdentity[] identities = content.getValue(_getRoleAttributePath()); 556 if (identities != null && identities.length > 0) 557 { 558 Map<UserIdentity, AccessExplanation> contextExplanation = new HashMap<>(); 559 for (UserIdentity userIdentity: identities) 560 { 561 contextExplanation.put(userIdentity, _explainPermissionForRole(userIdentity, content, false)); 562 } 563 result.put(getExplanationObject(content), contextExplanation); 564 } 565 } 566 return result; 567 } 568 } 569 return Map.of(); 570 } 571 572 private record PermissionDetails(AccessResult result, Content object, boolean inherited) { } 573}