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}