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}