001/* 002 * Copyright 2026 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.cms.rights; 017 018import java.util.HashMap; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022 023import org.apache.avalon.framework.activity.Initializable; 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.avalon.framework.service.Serviceable; 027 028import org.ametys.cms.content.RootContentHelper; 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.RightManager; 035import org.ametys.core.right.RightsException; 036import org.ametys.core.user.UserIdentity; 037import org.ametys.plugins.repository.AmetysObjectResolver; 038import org.ametys.runtime.config.Config; 039import org.ametys.runtime.i18n.I18nizableText; 040import org.ametys.runtime.plugin.component.PluginAware; 041 042/** 043 * Access controller for content reactions and report. 044 * The access to content reactions is managed by two rights: "CMS_Rights_Content_Report" and "CMS_Rights_Content_React". 045 * If the content interaction access is based on read access, any user (including anonymous) with read access will be able to react or report a content. 046 */ 047public class ContentInteractionAccessController implements AccessController, Initializable, Serviceable, PluginAware 048{ 049 private static final List<String> __REACTION_RIGHTS = List.of("CMS_Rights_Content_Report", "CMS_Rights_Content_React"); 050 051 /** The helper for root content */ 052 protected RootContentHelper _rootContentHelper; 053 /** The Ametys object resolver */ 054 protected AmetysObjectResolver _resolver; 055 056 private boolean _readAccessBased; 057 058 private String _id; 059 060 private RightManager _rightManager; 061 062 @Override 063 public void initialize() throws Exception 064 { 065 _readAccessBased = Config.getInstance().getValue("cms.contents.interaction.read.access.based", true, true); 066 } 067 068 public void service(ServiceManager manager) throws ServiceException 069 { 070 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 071 _rootContentHelper = (RootContentHelper) manager.lookup(RootContentHelper.ROLE); 072 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 073 } 074 075 public void setPluginInfo(String pluginName, String featureName, String id) 076 { 077 _id = id; 078 } 079 080 public boolean supports(Object object) 081 { 082 return object instanceof Content; 083 } 084 085 public String getId() 086 { 087 return _id; 088 } 089 090 /** 091 * Returns true if the content interaction access is based on read access, false if it is based on specific rights only. 092 * @param object The content object 093 * @return The reaction access type 094 */ 095 protected boolean isReadAccessBased(Object object) 096 { 097 return _readAccessBased; 098 } 099 100 @Override 101 public AccessResult getPermissionForAnonymous(String rightId, Object object) 102 { 103 if (__REACTION_RIGHTS.contains(rightId) && isReadAccessBased(object)) 104 { 105 if (_rightManager.hasAnonymousReadAccess(object)) 106 { 107 return AccessResult.ANONYMOUS_ALLOWED; 108 } 109 } 110 111 return AccessResult.UNKNOWN; 112 } 113 114 @Override 115 public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object) 116 { 117 if (__REACTION_RIGHTS.contains(rightId) && isReadAccessBased(object)) 118 { 119 if (_rightManager.hasAnonymousReadAccess(object)) 120 { 121 return AccessResult.ANONYMOUS_ALLOWED; 122 } 123 124 if (_rightManager.hasAnyConnectedUserReadAccess(object)) 125 { 126 return AccessResult.ANY_CONNECTED_ALLOWED; 127 } 128 } 129 130 return AccessResult.UNKNOWN; 131 } 132 133 @Override 134 public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object) 135 { 136 if (__REACTION_RIGHTS.contains(rightId) && isReadAccessBased(object)) 137 { 138 if (_rightManager.hasAnonymousReadAccess(object)) 139 { 140 return AccessResult.ANONYMOUS_ALLOWED; 141 } 142 143 if (_rightManager.hasAnyConnectedUserReadAccess(object)) 144 { 145 return AccessResult.ANY_CONNECTED_ALLOWED; 146 } 147 148 if (_rightManager.hasReadAccess(user, object)) 149 { 150 return AccessResult.USER_ALLOWED; 151 } 152 } 153 154 return AccessResult.UNKNOWN; 155 } 156 157 @Override 158 public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object) 159 { 160 Map<UserIdentity, AccessResult> permissionsByUser = new HashMap<>(); 161 162 if (__REACTION_RIGHTS.contains(rightId) && isReadAccessBased(object)) 163 { 164 _rightManager.getReadAccessAllowedUsers(object).getAllowedUsers().forEach(user -> { 165 permissionsByUser.put(user, AccessResult.USER_ALLOWED); 166 }); 167 } 168 169 return permissionsByUser; 170 } 171 172 @Override 173 public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object) 174 { 175 Map<GroupIdentity, AccessResult> permissionsByGroup = new HashMap<>(); 176 177 if (__REACTION_RIGHTS.contains(rightId) && isReadAccessBased(object)) 178 { 179 _rightManager.getReadAccessAllowedUsers(object).getAllowedGroups().forEach(group -> { 180 permissionsByGroup.put(group, AccessResult.GROUP_ALLOWED); 181 }); 182 } 183 184 return permissionsByGroup; 185 } 186 187 public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object) 188 { 189 Map<String, AccessResult> permissionByRight = new HashMap<>(); 190 191 if (isReadAccessBased(object)) 192 { 193 // Access result is the same for all reaction rights, as they are all based on read access, so we can compute it once and apply it to all rights. 194 AccessResult accessResult = AccessResult.UNKNOWN; 195 if (_rightManager.hasAnonymousReadAccess(object)) 196 { 197 accessResult = AccessResult.ANONYMOUS_ALLOWED; 198 } 199 else if (_rightManager.hasAnyConnectedUserReadAccess(object)) 200 { 201 accessResult = AccessResult.ANY_CONNECTED_ALLOWED; 202 } 203 else if (_rightManager.hasReadAccess(user, object)) 204 { 205 accessResult = AccessResult.USER_ALLOWED; 206 } 207 208 if (accessResult != AccessResult.UNKNOWN) 209 { 210 for (String rightId : __REACTION_RIGHTS) 211 { 212 permissionByRight.put(rightId, accessResult); 213 } 214 } 215 } 216 217 return permissionByRight; 218 } 219 220 // ---------------------------------------------------------------------------------------- 221 // Right's explanation methods 222 223 @Override 224 public I18nizableText getObjectCategory(Object object) 225 { 226 return ContentAccessController.CONTENT_CONTEXT_CATEGORY; 227 } 228 229 @Override 230 public I18nizableText getObjectLabel(Object object) throws RightsException 231 { 232 if (object instanceof Content content) 233 { 234 return new I18nizableText(content.getTitle()); 235 } 236 throw new RightsException("unsupported context: " + object.toString()); 237 } 238 239 public AccessExplanation explainReadAccessPermissionForAnonymous(Object object) 240 { 241 return _getAccessExplanation(getReadAccessPermissionForAnonymous(object), object); 242 } 243 244 @Override 245 public AccessExplanation explainReadAccessPermission(UserIdentity user, Set<GroupIdentity> groups, Object object) 246 { 247 return _getAccessExplanation(getReadAccessPermission(user, groups, object), object); 248 } 249 250 @Override 251 public AccessExplanation explainPermissionForAnonymous(String rightId, Object object) 252 { 253 return _getAccessExplanation(getPermissionForAnonymous(rightId, object), object); 254 } 255 256 public AccessExplanation explainPermission(UserIdentity user, Set<GroupIdentity> groups, String rightId, Object object) 257 { 258 return _getAccessExplanation(getPermission(user, groups, rightId, object), object); 259 } 260 261 @Override 262 public Map<ExplanationObject, Map<Permission, AccessExplanation>> explainAllPermissions(UserIdentity identity, Set<GroupIdentity> groups, Set<Object> workspacesContexts) 263 { 264 if (isReadAccessBased(null)) 265 { 266 Map<ExplanationObject, Map<Permission, AccessExplanation>> result = new HashMap<>(); 267 268 // Simplify the explanation by providing a single (fake) context describing every content with read access instead of listing all of them 269 ExplanationObject allContentsCtx = new ExplanationObject( 270 "all-contents-in-readaccess", 271 new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_INTERACTION_ACCESS_CONTROLLER_ALL_CONTENTS_IN_READ_ACCESS_CONTEXT_LABEL"), 272 getObjectCategory(null), 273 10 274 ); 275 276 Map<Permission, AccessExplanation> permissions = new HashMap<>(); 277 278 // Explanation is the same for all reaction rights, as they are all based on read access, so we can compute it once and apply it to all rights. 279 // Use the ANONYMOUS_ALLOWED access result, as it is the most permissive access result and the explanation label is the same for all access results based on read access. 280 AccessExplanation explanation = _getAccessExplanation(AccessResult.ANONYMOUS_ALLOWED, null); 281 for (String rightId : __REACTION_RIGHTS) 282 { 283 permissions.put(new Permission(PermissionType.RIGHT, rightId), explanation); 284 } 285 result.put(allContentsCtx, permissions); 286 287 return result; 288 } 289 return Map.of(); 290 } 291 292 public Map<Permission, AccessExplanation> explainAllPermissionsForAnonymous(Object object) 293 { 294 Map<Permission, AccessExplanation> results = new HashMap<>(); 295 296 for (String rightId : __REACTION_RIGHTS) 297 { 298 AccessResult accessResult = getPermissionForAnonymous(rightId, object); 299 if (accessResult != AccessResult.UNKNOWN) 300 { 301 results.put(new Permission(PermissionType.RIGHT, rightId), _getAccessExplanation(accessResult, object)); 302 } 303 } 304 305 return results; 306 } 307 308 public Map<Permission, AccessExplanation> explainAllPermissionsForAnyConnected(Object object) 309 { 310 Map<Permission, AccessExplanation> results = new HashMap<>(); 311 312 for (String rightId : __REACTION_RIGHTS) 313 { 314 AccessResult accessResult = getPermissionForAnyConnectedUser(rightId, object); 315 if (accessResult != AccessResult.UNKNOWN) 316 { 317 results.put(new Permission(PermissionType.RIGHT, rightId), _getAccessExplanation(accessResult, object)); 318 } 319 } 320 321 return results; 322 } 323 324 public Map<UserIdentity, Map<Permission, AccessExplanation>> explainAllPermissionsByUser(Object object) 325 { 326 Map<UserIdentity, Map<Permission, AccessExplanation>> permissionsByUser = new HashMap<>(); 327 328 if (isReadAccessBased(object)) 329 { 330 for (String rightId : __REACTION_RIGHTS) 331 { 332 getPermissionByUser(rightId, object).forEach((user, accessResult) -> { 333 permissionsByUser.computeIfAbsent(user, k -> new HashMap<>()).put(new Permission(PermissionType.RIGHT, rightId), _getAccessExplanation(accessResult, object)); 334 }); 335 336 } 337 } 338 339 return permissionsByUser; 340 } 341 342 public Map<GroupIdentity, Map<Permission, AccessExplanation>> explainAllPermissionsByGroup(Object object) 343 { 344 Map<GroupIdentity, Map<Permission, AccessExplanation>> permissionsByGroup = new HashMap<>(); 345 346 if (isReadAccessBased(object)) 347 { 348 for (String rightId : __REACTION_RIGHTS) 349 { 350 getPermissionByGroup(rightId, object).forEach((group, accessResult) -> { 351 permissionsByGroup.computeIfAbsent(group, k -> new HashMap<>()).put(new Permission(PermissionType.RIGHT, rightId), _getAccessExplanation(accessResult, object)); 352 }); 353 354 } 355 } 356 357 return permissionsByGroup; 358 } 359 360 /** 361 * Get the access explanation for given access result 362 * @param result the access result 363 * @param object the object. Can be null 364 * @return The access explanation 365 */ 366 protected AccessExplanation _getAccessExplanation(AccessResult result, Object object) 367 { 368 switch (result) 369 { 370 case ANONYMOUS_ALLOWED: 371 case ANY_CONNECTED_ALLOWED: 372 case USER_ALLOWED: 373 case UNKNOWN: 374 return new AccessExplanation( 375 getId(), 376 result, 377 new I18nizableText( 378 "plugin.cms", 379 "PLUGINS_CMS_CONTENT_INTERACTION_ACCESS_CONTROLLER_" + result.name() + "_EXPLANATION", 380 object != null ? Map.of("title", getObjectLabel(object)) : Map.of() 381 ) 382 ); 383 default: 384 return AccessController.getDefaultAccessExplanation(getId(), result); 385 } 386 } 387 388 // ------------------------------------------------------------------------------ 389 // Read access is not managed by this controller, so return UNKNOWN for all cases 390 391 @Override 392 public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object) 393 { 394 return AccessResult.UNKNOWN; 395 } 396 397 @Override 398 public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object) 399 { 400 return Map.of(); 401 } 402 403 @Override 404 public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object) 405 { 406 return Map.of(); 407 } 408 409 @Override 410 public AccessResult getReadAccessPermissionForAnonymous(Object object) 411 { 412 return AccessResult.UNKNOWN; 413 } 414 415 @Override 416 public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object) 417 { 418 return AccessResult.UNKNOWN; 419 } 420 421 // ------------------------------------------------------------------------------ 422 // No workspace permission 423 424 public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId) 425 { 426 return false; 427 } 428 429 public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId) 430 { 431 return false; 432 } 433 434 public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId) 435 { 436 return false; 437 } 438 439 public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts) 440 { 441 return false; 442 } 443 444 public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts) 445 { 446 return false; 447 } 448 449 public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups) 450 { 451 return false; 452 } 453}