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}