001/*
002 *  Copyright 2016 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.core.right;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Comparator;
021import java.util.Map;
022import java.util.Set;
023
024import org.apache.commons.lang3.StringUtils;
025
026import org.ametys.core.group.GroupIdentity;
027import org.ametys.core.right.RightManager.RightResult;
028import org.ametys.core.user.UserIdentity;
029import org.ametys.runtime.i18n.I18nizableText;
030
031/**
032 * This interface is for computing the rights a user has.
033 */
034public interface AccessController
035{
036    /**
037     * The access result when looking for a right
038     */
039    public enum AccessResult
040    {
041        /* Note: the order of the values are important ! */
042        
043        /** If the user is allowed because the right is allowed for any anonymous user */
044        ANONYMOUS_ALLOWED,
045        /** If the user is directly denied */
046        USER_DENIED,
047        /** If the user is directly allowed */
048        USER_ALLOWED,
049        /** If the user is denied through its groups and not directly */
050        GROUP_DENIED,
051        /** If the user is allowed through its groups and not directly */
052        GROUP_ALLOWED,
053        /** If the user is denied because the right is denied for any connected user */
054        ANY_CONNECTED_DENIED,
055        /** If the user is allowed because the right is allowed for any connected user */
056        ANY_CONNECTED_ALLOWED,
057        /** If the user is denied because the right is denied for any anonymous user */
058        ANONYMOUS_DENIED,
059        /** Cannot determine */
060        UNKNOWN;
061        
062        /**
063         * Convert the value to an access result
064         * @return denied enumerated will be converted to deny, allow to allow and unknown to unknown
065         */
066        public RightResult toRightResult()
067        {
068            switch (this)
069            {
070                case USER_DENIED:
071                case GROUP_DENIED:
072                case ANY_CONNECTED_DENIED:
073                case ANONYMOUS_DENIED:
074                    return RightResult.RIGHT_DENY;
075                case USER_ALLOWED:
076                case GROUP_ALLOWED:
077                case ANY_CONNECTED_ALLOWED:
078                case ANONYMOUS_ALLOWED:
079                    return RightResult.RIGHT_ALLOW;
080                case UNKNOWN:
081                default:
082                    return RightResult.RIGHT_UNKNOWN;
083            }
084        }
085        
086        /**
087         * Merge several access results to keep only the more important
088         * @param accessResults The access results to merge. Cannot be null but can be empty.
089         * @return The more important access result
090         */
091        public static AccessResult merge(Collection<AccessResult> accessResults)
092        {
093            return accessResults.stream().filter(r -> r != null).min(Comparator.naturalOrder()).orElse(AccessResult.UNKNOWN);
094        }
095        
096        /**
097         * Merge several access results to keep only the more important
098         * @param accessResults The access results to merge. Cannot be null but can be empty.
099         * @return The more important access result
100         */
101        public static AccessResult merge(AccessResult... accessResults)
102        {
103            return Arrays.stream(accessResults).filter(r -> r != null).min(Comparator.naturalOrder()).orElse(AccessResult.UNKNOWN);
104        }
105    }
106    
107    /**
108     * Get the id of this controller
109     * @return the id  of this controller
110     */
111    public String getId();
112    
113    /**
114     * Gets the kind of access a user has on an object for a given right
115     * @param user The user. Cannot be null.
116     * @param userGroups The groups the user belongs to
117     * @param rightId The id of the right of the user
118     * @param object The context object to check the access
119     * @return the kind of access a user has on an object for a right
120     */
121    public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object);
122
123    /**
124     * Gets the kind of access a user has on an object for thye read access
125     * @param user The user. Cannot be null.
126     * @param userGroups The groups the user belongs to
127     * @param object The context object to check the access
128     * @return the kind of access a user has on an object for the read access
129     */
130    public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object);
131
132    /**
133     * Gets the kind of access a user has on an object for all rights
134     * @param user The user. Cannot be null.
135     * @param userGroups The groups the user belongs to
136     * @param object The context object to check the access
137     * @return the kind of access a user has on an object for all rights
138     */
139    public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object);
140    
141    /**
142     * Gets the permission for Anonymous only on an object for a given right
143     * @param rightId The id of the right to check
144     * @param object The object
145     * @return the permission for Anonymous only on an object for a given right
146     */
147    public AccessResult getPermissionForAnonymous(String rightId, Object object);
148    
149    /**
150     * Gets the read access permission for Anonymous only on an object
151     * @param object The object
152     * @return the read access permission for Anonymous only on an object
153     */
154    public AccessResult getReadAccessPermissionForAnonymous(Object object);
155    
156    /**
157     * Gets the permission for any connected user only on an object for a given right
158     * @param rightId The id of the right to check
159     * @param object The object
160     * @return the permission for any connected user only on an object for a given right
161     */
162    public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object);
163
164    /**
165     * Gets the read access permission for any connected user only on an object
166     * @param object The object
167     * @return the read access permission for any connected user only on an object
168     */
169    public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object);
170
171    /**
172     * Gets the permission by user only on an object for the given right. It does not take account of the groups of the user, etc.
173     * @param rightId The id of the right to check
174     * @param object The object
175     * @return the permission by user only on an object for the given right
176     */
177    public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object);
178
179    /**
180     * Gets the read access permission by user only on an object. It does not take account of the groups of the user, etc.
181     * @param object The object
182     * @return the read access permission by user only on an object
183     */
184    public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object);
185    
186    /**
187     * Gets the permission by group only on an object for the given right.
188     * @param rightId The id of the right to check
189     * @param object The object
190     * @return the permission by group only on an object for the given right
191     */
192    public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object);
193    
194    /**
195     * Gets the read access permission by group only on an object.
196     * @param object The object
197     * @return the read access permission by group only on an object
198     */
199    public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object);
200    
201    /**
202     * Returns true if the user has a permission on at least one object, directly or though groups, for a given rights and if the object is attached to the given context that is /${WorkspaceName} and its conversions.<br>
203     * @param workspacesContexts The contexts to tests such as {"/${WorkspaceName}", "/repository", "/admin"}
204     * @param user The user
205     * @param userGroups The groups
206     * @param rightId The id of the right to check
207     * @return true if the user has a permission on at least one object, directly or though groups, for a given right
208     */
209    public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId);
210    
211    /**
212     * Returns true if the user has a read access permission on at least one object, directly or though groups, for a given rights and if the object is attached to the given context that is /${WorkspaceName} and its conversions.<br>
213     * @param workspacesContexts The contexts to tests such as {"/${WorkspaceName}", "/repository", "/admin"}
214     * @param user The user
215     * @param userGroups The groups
216     * @return true if the user has a permission on at least one object, directly or though groups, for a given right
217     */
218    public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups);
219    
220    /**
221     * Returns true if anonymous has a permission on at least one object, directly or though groups, for a given rights and if the object is attached to the given context that is /${WorkspaceName} and its conversions.<br>
222     * @param workspacesContexts The contexts to tests such as {"/${WorkspaceName}", "/repository", "/admin"}
223     * @param rightId The id of the right to check
224     * @return true if anonymous has a permission on at least one object, directly or though groups, for a given right
225     */
226    public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId);
227    
228    /**
229     * Returns true if anonymous has a read access permission on at least one object, directly or though groups, for a given rights and if the object is attached to the given context that is /${WorkspaceName} and its conversions.<br>
230     * @param workspacesContexts The contexts to tests such as {"/${WorkspaceName}", "/repository", "/admin"}
231     * @return true if anonymous has a permission on at least one object, directly or though groups, for a given right
232     */
233    public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts);
234    
235    /**
236     * Returns true if any connected user has a permission on at least one object, directly or though groups, for a given rights and if the object is attached to the given context that is /${WorkspaceName} and its conversions.<br>
237     * @param workspacesContexts The contexts to tests such as {"/${WorkspaceName}", "/repository", "/admin"}
238     * @param rightId The id of the right to check
239     * @return true if any connected user has a permission on at least one object, directly or though groups, for a given right
240     */
241    public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId);
242    
243    /**
244     * Returns true if any connected user has a read access permission on at least one object, directly or though groups, for a given rights and if the object is attached to the given context that is /${WorkspaceName} and its conversions.<br>
245     * @param workspacesContexts The contexts to tests such as {"/${WorkspaceName}", "/repository", "/admin"}
246     * @return true if any connected user has a permission on at least one object, directly or though groups, for a given right
247     */
248    public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts);
249    
250    /**
251     * Returns true if this access controller supports the given object
252     * @param object The object to test
253     * @return true if this access controller supports the given object
254     */
255    public boolean isSupported(Object object);
256    
257    /**
258     * Explain the read access permission for anonymous on the given object.
259     * 
260     * The access result in the explanation MUST be the same value as the one returned by
261     * {@link #getReadAccessPermissionForAnonymous(Object)}. And the explanation should described the
262     * actual object that granted the right to allow final user to see if any context conversion happened
263     * @param object the object to test
264     * @return an explanation of the access
265     */
266    public default AccessExplanation explainReadAccessPermissionForAnonymous(Object object)
267    {
268        return getStandardAccessExplanation(getReadAccessPermissionForAnonymous(object), object);
269    }
270    /**
271     * Explain the permission for anonymous on the given object.
272     * 
273     * The access result in the explanation MUST be the same value as the one returned by
274     * {@link #getPermissionForAnonymous(String, Object)}. And the explanation should described the
275     * actual object that granted the right to allow final user to see if any context conversion happened
276     * @param rightId the right to test
277     * @param object the object to test
278     * @return an explanation of the access
279     */
280    public default AccessExplanation explainPermissionForAnonymous(String rightId, Object object)
281    {
282        return getStandardAccessExplanation(getPermissionForAnonymous(rightId, object), object);
283    }
284
285    /**
286     * Explain the read access permission for any connected user on the given object.
287     * 
288     * The access result in the explanation MUST be the same value as the one returned by
289     * {@link #getReadAccessPermissionForAnyConnectedUser(Object)}. And the explanation should described the
290     * actual object that granted the right to allow final user to see if any context conversion happened
291     * @param object the object to test
292     * @return an explanation of the access
293     */
294    public default AccessExplanation explainReadAccessPermissionForAnyConnectedUser(Object object)
295    {
296        return getStandardAccessExplanation(getReadAccessPermissionForAnyConnectedUser(object), object);
297    }
298
299    /**
300     * Explain the permission for any connected user on the given object.
301     * 
302     * The access result in the explanation MUST be the same value as the one returned by
303     * {@link #getPermissionForAnyConnectedUser(String, Object)}. And the explanation should described the
304     * actual object that granted the right to allow final user to see if any context conversion happened
305     * @param rightId the right to test
306     * @param object the object to test
307     * @return an explanation of the access
308     */
309    public default AccessExplanation explainPermissionForAnyConnectedUser(String rightId, Object object)
310    {
311        return getStandardAccessExplanation(getPermissionForAnyConnectedUser(rightId, object), object);
312    }
313
314    /**
315     * Explain the read access permission for a user on the given object.
316     * 
317     * The access result in the explanation MUST be the same value as the one returned by
318     * {@link #getReadAccessPermission(UserIdentity, Set, Object)}. And the explanation should described the
319     * actual object that granted the right to allow final user to see if any context conversion happened
320     * @param user the user to test
321     * @param groups the groups of the user
322     * @param object the object to test
323     * @return an explanation of the access
324     */
325    public default AccessExplanation explainReadAccessPermission(UserIdentity user, Set<GroupIdentity> groups, Object object)
326    {
327        return getStandardAccessExplanation(getReadAccessPermission(user, groups, object), object);
328    }
329    
330    /**
331     * Explain the permission for a user on the given object.
332     * 
333     * The access result in the explanation MUST be the same value as the one returned by
334     * {@link #getPermission(UserIdentity, Set, String, Object)}. And the explanation should described the
335     * actual object that granted the right to allow final user to see if any context conversion happened
336     * @param user the user to test
337     * @param groups the groups of the user
338     * @param rightId the right to test
339     * @param object the object to test
340     * @return an explanation of the access
341     */
342    public default AccessExplanation explainPermission(UserIdentity user, Set<GroupIdentity> groups, String rightId, Object object)
343    {
344        return getStandardAccessExplanation(getPermission(user, groups, rightId, object), object);
345    }
346
347    /**
348     * Get a standard explanation based on the access result
349     * @param accessResult the access result
350     * @param object the inspected context
351     * @return the explanation
352     */
353    public default AccessExplanation getStandardAccessExplanation(AccessResult accessResult, Object object)
354    {
355        return AccessController.getDefaultAccessExplanation(getId(), accessResult);
356    }
357    
358    /**
359     * Build a default explanation for an access result provided by a controller.
360     * 
361     * This method should be used as a fallback.
362     * AccessController should provide their own explanation with more details
363     * @param controllerId the access controller id
364     * @param result the access result
365     * @return an label describing the result
366     */
367    public static AccessExplanation getDefaultAccessExplanation(String controllerId, AccessResult result)
368    {
369        I18nizableText label = new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHTS_EXPLAIN_TOOL_RESULT_" + result.name(), Map.of("controllerId", new I18nizableText(controllerId)));
370        return new AccessExplanation(controllerId, result, label);
371    }
372    
373    /**
374     * Get {@link AccessExplanation} for each permission given to the user by this access controller.
375     * 
376     * Returns a pair of permission/access explanation for each object with a granted or denied permission to this user by this access controller.
377     * 
378     * Each explanation should be equivalent to calling the {@link #explainPermission(UserIdentity, Set, String, Object)}
379     * or {@link #explainReadAccessPermission(UserIdentity, Set, Object)} for the user, on the object with the corresponding right
380     * 
381     * @param identity the user identity
382     * @param groups the groups the user belongs to.
383     * @return all the user's permissions handled by this controller
384     */
385    public Map<ExplanationObject, Map<Permission, AccessExplanation>> explainAllPermissions(UserIdentity identity, Set<GroupIdentity> groups);
386    
387    
388    /**
389     * Get the explanation object representing the object
390     * @param object the object
391     * @return the explanation object
392     */
393    public default ExplanationObject getExplanationObject(Object object)
394    {
395        return new ExplanationObject(
396                object,
397                getObjectLabel(object),
398                getObjectCategory(object),
399                getObjectPriority(object)
400                );
401    }
402    
403    /**
404     * Get a label describing the object handled by this access controller
405     * @param object the object
406     * @return the label
407     */
408    public abstract I18nizableText getObjectLabel(Object object);
409    
410    
411    /**
412     * Get a label classifying the object handled by this access controller
413     * @param object the object
414     * @return the label
415     */
416    public I18nizableText getObjectCategory(Object object);
417
418    
419    /**
420     * Get the priority of the object to order it in its category
421     * @param object the object
422     * @return the priority
423     */
424    public default int getObjectPriority(Object object)
425    {
426        return 0;
427    }
428    
429
430    /**
431     * A object with an associated label, category and order.
432     * 
433     * @param object the object
434     * @param label a label for the context represented by the object
435     * @param category the category the context belongs to
436     * @param order order of the context in the category
437     */
438    public record ExplanationObject(Object object, I18nizableText label, I18nizableText category, int order) {
439        /**
440         * An object with an associated label and category.
441         * 
442         * @param object the object
443         * @param label a label for the context represented by the object
444         * @param category the category the context belongs to
445         */
446        public ExplanationObject(Object object, I18nizableText label, I18nizableText category)
447        {
448            this(object, label, category, 0);
449        }
450        
451        @Override
452        public final boolean equals(Object other)
453        {
454            if (other instanceof ExplanationObject otherContext)
455            {
456                return object.equals(otherContext.object())
457                        && (category == null && otherContext.category() == null
458                            || category != null && category.equals(otherContext.category()));
459            }
460            return false;
461        }
462        
463        @Override
464        public final int hashCode()
465        {
466            return object.hashCode() + (category != null ? 23 * category.hashCode() : 0);
467        }
468    }
469    
470    /**
471     * A permission given by an AccessController
472     * @param type the type of permission given
473     * @param id the id of the permission (right's id or profile's id). Can be null for types {@link PermissionType#READ} or {@link PermissionType#ALL_RIGHTS}
474     */
475    public record Permission(PermissionType type, String id) {
476        @Override
477        public String toString()
478        {
479            if (StringUtils.isNotBlank(id))
480            {
481                return type.name() + "#" + id;
482            }
483            else
484            {
485                return type.name();
486            }
487        }
488        /**
489         * A type of permission
490         */
491        public enum PermissionType
492        {
493            /** Read access */
494            READ,
495            /** A single right */
496            RIGHT,
497            /** A profile */
498            PROFILE,
499            /** All rights */
500            ALL_RIGHTS
501        }
502    }
503}