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