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.HashSet;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.commons.lang.StringUtils;
029
030import org.ametys.cms.repository.Content;
031import org.ametys.core.group.GroupIdentity;
032import org.ametys.core.right.AccessController;
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.data.EducationalPath;
039import org.ametys.odf.orgunit.OrgUnit;
040import org.ametys.odf.rights.ODFRightHelper.ContextualizedContent;
041import org.ametys.odf.rights.ODFRightHelper.ContextualizedPermissionContext;
042import org.ametys.odf.tree.ODFContentsTreeHelper;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.runtime.i18n.I18nizableText;
045import org.ametys.runtime.plugin.component.PluginAware;
046
047/**
048 * {@link AccessController} for a ODF {@link ContextualizedContent} based on ODF role
049 *
050 */
051public abstract class AbstractODFRoleForContextualizedContentAccessController implements AccessController, Serviceable, PluginAware
052{
053    /** The rights profile DAO */
054    protected RightProfilesDAO _rightProfileDAO;
055    /** The ODF contents tree helper */
056    protected ODFContentsTreeHelper _odfContentsTreeHelper;
057    /** The ametys resolver */
058    protected AmetysObjectResolver _resolver;
059    /** The ODF right helper */
060    protected ODFRightHelper _odfRightHelper;
061
062    private String _id;
063    
064    public void service(ServiceManager smanager) throws ServiceException
065    {
066        _rightProfileDAO = (RightProfilesDAO) smanager.lookup(RightProfilesDAO.ROLE);
067        _odfContentsTreeHelper = (ODFContentsTreeHelper) smanager.lookup(ODFContentsTreeHelper.ROLE);
068        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
069        _odfRightHelper = (ODFRightHelper) smanager.lookup(org.ametys.odf.rights.ODFRightHelper.ROLE);
070    }
071    
072    public void setPluginInfo(String pluginName, String featureName, String id)
073    {
074        _id = id;
075    }
076    
077    public String getId()
078    {
079        return _id;
080    }
081    
082    public boolean supports(Object object)
083    {
084        return object instanceof ContextualizedContent;
085    }
086    
087    /**
088     * Get the parents of the content for rights purpose
089     * @param content the content
090     * @param permissionCtx the permission context
091     * @return the parents of content
092     */
093    protected Set<Content> getParents(Content content, ContextualizedPermissionContext permissionCtx)
094    {
095        if (content instanceof ProgramItem programItem)
096        {
097            EducationalPath educationalPath = permissionCtx.getEducationalPath();
098            if (educationalPath == null)
099            {
100                return Set.of();
101            }
102            
103            List<ProgramItem> programItemsInPath = educationalPath.getProgramItems(_resolver);
104            
105            ProgramItem parent = programItemsInPath.getLast();
106            
107            // Update educational path in permission contexte
108            permissionCtx.withEducationalPath(_removeLast(educationalPath));
109            
110            
111            Set<Content> parents = new HashSet<>();
112            parents.add((Content) parent);
113            
114            // Add orgunits
115            List<String> ouIds = programItem.getOrgUnits();
116            parents.addAll(ouIds.stream()
117                    .filter(Objects::nonNull)
118                    .filter(_resolver::hasAmetysObjectForId)
119                    .map(_resolver::resolveById)
120                    .map(OrgUnit.class::cast)
121                    .collect(Collectors.toSet()));
122            
123            return parents;
124        }
125        else if (content instanceof OrgUnit ou)
126        {
127            OrgUnit parentOrgUnit = ou.getParentOrgUnit();
128            if (parentOrgUnit != null)
129            {
130                return Set.of(parentOrgUnit);
131            }
132        }
133        
134        return Set.of();
135    }
136    
137    /**
138     * Get the permission context
139     * @param contextualizedContent the initial contextualized content
140     * @return the permission context.
141     */
142    protected ContextualizedPermissionContext getPermissionContext(ContextualizedContent contextualizedContent)
143    {
144        EducationalPath educationalPath = contextualizedContent.path();
145        Content initialContent = contextualizedContent.content();
146        
147        // Remove initial content from education path
148        List<ProgramItem> programItemsInPath = educationalPath.getProgramItems(_resolver);
149        if (programItemsInPath.getLast().getId().equals(initialContent.getId()))
150        {
151            educationalPath = _removeLast(educationalPath);
152        }
153        return new ContextualizedPermissionContext(initialContent, educationalPath);
154    }
155    
156    private EducationalPath _removeLast(EducationalPath educationalPath)
157    {
158        List<ProgramItem> programItemsInPath = educationalPath.getProgramItems(_resolver);
159        List<ProgramItem> subList = programItemsInPath.subList(0, programItemsInPath.size() - 1);
160        if (!subList.isEmpty())
161        {
162            return EducationalPath.of(subList.toArray(ProgramItem[]::new));
163        }
164        return null;
165    }
166    
167    public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object)
168    {
169        if (object instanceof ContextualizedContent contextualizedContent)
170        {
171            return _getPermission(user, userGroups, rightId, contextualizedContent.content(), getPermissionContext(contextualizedContent));
172        }
173        
174        return AccessResult.UNKNOWN;
175    }
176    
177    private AccessResult _getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Content object, ContextualizedPermissionContext permissionCtx)
178    {
179        List<String> rights = getRightsInTargetProfile();
180        if (rights.contains(rightId))
181        {
182            Set<UserIdentity> allowedUsers = getLocalAllowedUsers(object);
183            if (allowedUsers.contains(user))
184            {
185                return AccessResult.USER_ALLOWED;
186            }
187        }
188        
189        AccessResult permission = AccessResult.UNKNOWN;
190        
191        Set<Content> parents = getParents(object, permissionCtx);
192        if (parents != null)
193        {
194            for (Content parent : parents)
195            {
196                AccessResult parentResult = _getPermission(user, userGroups, rightId, parent, permissionCtx);
197                permission = AccessResult.merge(permission, parentResult);
198            }
199        }
200        
201        return permission;
202    }
203    
204    /**
205     * Get the rights hold by target profile
206     * @return the rights hold by target profile
207     */
208    protected synchronized List<String> getRightsInTargetProfile()
209    {
210        String profileId = getTargetProfileId();
211        return StringUtils.isNotBlank(profileId) ? _rightProfileDAO.getRights(profileId) : List.of();
212    }
213    
214    /**
215     * Get the id of target profile
216     * @return the id of target profile
217     */
218    protected abstract String getTargetProfileId();
219    
220    /**
221     * Get the allowed users for this content taking into account the content itself and its parents
222     * @param content the ODF content (program item or orgunit)
223     * @param permissionCtx the permission context
224     * @return the allowed users. Empty if no user is allowed on this content
225     */
226    protected Set<UserIdentity> getAllowedUsers(Content content, ContextualizedPermissionContext permissionCtx)
227    {
228        Set<UserIdentity> allowedUsers = getLocalAllowedUsers(content);
229        
230        Set<Content> parents = getParents(content, permissionCtx);
231        if (parents != null)
232        {
233            for (Content parent : parents)
234            {
235                allowedUsers.addAll(getAllowedUsers(parent, permissionCtx));
236            }
237        }
238        
239        return allowedUsers;
240    }
241    
242    /**
243     * Get the local allowed users for this content
244     * @param content the ODF content (program item or orgunit)
245     * @return the allowed users. Empty if no user is allowed on this content
246     */
247    protected abstract Set<UserIdentity> getLocalAllowedUsers(Content content);
248
249    public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object)
250    {
251        return AccessResult.UNKNOWN;
252    }
253
254    public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object)
255    {
256        if (object instanceof ContextualizedContent contextualizedContent)
257        {
258            Set<UserIdentity> allowedUsers = getAllowedUsers((Content) object, getPermissionContext(contextualizedContent));
259            if (allowedUsers.contains(user))
260            {
261                return getRightsInTargetProfile().stream()
262                    .collect(Collectors.toMap(r -> r, r -> AccessResult.USER_ALLOWED));
263            }
264        }
265        
266        return Map.of();
267    }
268    
269    /**
270     * Get the attribute path for role
271     * @return the attribute path for role
272     */
273    protected abstract String getRoleAttributePath();
274
275    public AccessResult getPermissionForAnonymous(String rightId, Object object)
276    {
277        return AccessResult.UNKNOWN;
278    }
279
280    public AccessResult getReadAccessPermissionForAnonymous(Object object)
281    {
282        return AccessResult.UNKNOWN;
283    }
284
285    public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object)
286    {
287        return AccessResult.UNKNOWN;
288    }
289
290    public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object)
291    {
292        return AccessResult.UNKNOWN;
293    }
294
295    public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object)
296    {
297        if (object instanceof ContextualizedContent contextualizedContent && getRightsInTargetProfile().contains(rightId))
298        {
299            Set<UserIdentity> allowedUsers = getAllowedUsers((Content) object, getPermissionContext(contextualizedContent));
300            if (allowedUsers != null)
301            {
302                return allowedUsers.stream()
303                    .collect(Collectors.toMap(user -> user, user -> AccessResult.USER_ALLOWED));
304            }
305        }
306        return Map.of();
307    }
308
309    public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object)
310    {
311        return Map.of();
312    }
313
314    public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object)
315    {
316        return Map.of();
317    }
318
319    public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object)
320    {
321        return Map.of();
322    }
323
324    public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId)
325    {
326        return false;
327    }
328    
329    public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups)
330    {
331        return false;
332    }
333    
334    public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId)
335    {
336        return false;
337    }
338
339    public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts)
340    {
341        return false;
342    }
343
344    public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId)
345    {
346        return false;
347    }
348
349    public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts)
350    {
351        return false;
352    }
353    
354    @Override
355    public AccessExplanation explainPermission(UserIdentity user, Set<GroupIdentity> groups, String rightId, Object object)
356    {
357        return AccessController.getDefaultAccessExplanation(getId(), AccessResult.UNKNOWN);
358    }
359    
360    @Override
361    public Map<ExplanationObject, Map<Permission, AccessExplanation>> explainAllPermissions(UserIdentity identity, Set<GroupIdentity> groups, Set<Object> workspacesContexts)
362    {
363        return Map.of();
364    }
365    
366    public I18nizableText getObjectLabel(Object object)
367    {
368        if (object instanceof Content content)
369        {
370            return ODFContentHierarchicalAccessController.getContentObjectLabel(content, _odfContentsTreeHelper);
371        }
372        throw new RightsException("Unsupported object: " + object.toString());
373    }
374    
375    public I18nizableText getObjectCategory(Object object)
376    {
377        return ODFContentHierarchicalAccessController.ODF_CONTEXT_CATEGORY;
378    }
379    
380    public Map<Permission, AccessExplanation> explainAllPermissionsForAnonymous(Object object)
381    {
382        return Map.of();
383    }
384    
385    public Map<Permission, AccessExplanation> explainAllPermissionsForAnyConnected(Object object)
386    {
387        return Map.of();
388    }
389    
390    public Map<UserIdentity, Map<Permission, AccessExplanation>> explainAllPermissionsByUser(Object object)
391    {
392        return Map.of();
393    }
394    
395    public Map<GroupIdentity, Map<Permission, AccessExplanation>> explainAllPermissionsByGroup(Object object)
396    {
397        return Map.of();
398    }
399}