001/*
002 *  Copyright 2021 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.plugins.joboffer.right;
017
018import java.util.Collection;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
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.cocoon.components.ContextHelper;
028import org.apache.commons.collections.MapUtils;
029import org.apache.commons.lang3.ArrayUtils;
030import org.apache.commons.lang3.StringUtils;
031
032import org.ametys.cms.contenttype.ContentTypesHelper;
033import org.ametys.cms.data.ContentValue;
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.repository.ContentQueryHelper;
036import org.ametys.cms.repository.ContentTypeExpression;
037import org.ametys.core.group.GroupIdentity;
038import org.ametys.core.right.AccessController;
039import org.ametys.core.right.AccessExplanation;
040import org.ametys.core.right.RightsException;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.plugins.core.impl.right.AbstractRightBasedAccessController;
043import org.ametys.plugins.joboffer.JobOfferConstants;
044import org.ametys.plugins.repository.AmetysObjectIterable;
045import org.ametys.plugins.repository.AmetysObjectResolver;
046import org.ametys.plugins.repository.query.expression.AndExpression;
047import org.ametys.plugins.repository.query.expression.Expression;
048import org.ametys.plugins.repository.query.expression.Expression.Operator;
049import org.ametys.plugins.repository.query.expression.ExpressionContext;
050import org.ametys.plugins.repository.query.expression.OrExpression;
051import org.ametys.plugins.repository.query.expression.StringExpression;
052import org.ametys.plugins.repository.query.expression.UserExpression;
053import org.ametys.runtime.i18n.I18nizableText;
054import org.ametys.web.WebHelper;
055import org.ametys.web.repository.SiteAwareAmetysObject;
056
057/**
058 * {@link AccessController} so responsible of a job offer can access and handle the applications
059 *
060 */
061public class ApplicationAccessController extends AbstractRightBasedAccessController implements Serviceable
062{
063    /** ContentTypes Helper */
064    protected ContentTypesHelper _cTypeHelper;
065    /** the ametys object resolver */
066    protected AmetysObjectResolver _resolver;
067    
068    public void service(ServiceManager smanager) throws ServiceException
069    {
070        _cTypeHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
071        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
072    }
073    
074    /**
075     * Get the rights for person in charge of a application content
076     * @return the list of allowed rights
077     */
078    protected List<String> getApplicationRights()
079    {
080        return List.of(
081                "Workflow_Right_Application_Edit",
082                "Workflow_Right_Application_Shortlist",
083                "Workflow_Right_Application_Disapprove");
084    }
085    
086    /**
087     * Determines if the current user is in charge of the current application
088     * @param user the user
089     * @param content the application content
090     * @return true if the current user is in charge
091     */
092    protected boolean isInCharge(UserIdentity user, Content content)
093    {
094        UserIdentity[] personsInCharge = getPersonInCharge(content);
095        return personsInCharge != null && ArrayUtils.contains(personsInCharge, user);
096    }
097    
098    /**
099     * Get the persons in charge of a application
100     * @param content the application content
101     * @return the persons in charge or null if not found or empty
102     */
103    protected UserIdentity[] getPersonInCharge(Content content)
104    {
105        if (content.hasDefinition(JobOfferConstants.JOB_APPLICATION_ATTRIBUTE_PATH_PERSON_IN_CHARGE))
106        {
107            return content.getValue(JobOfferConstants.JOB_APPLICATION_ATTRIBUTE_PATH_PERSON_IN_CHARGE);
108        }
109        
110        return null;
111    }
112    
113    public boolean supports(Object object)
114    {
115        return object instanceof Content && _cTypeHelper.isInstanceOf((Content) object, JobOfferConstants.JOB_APPLICATION_CONTENT_TYPE);
116    }
117    
118    public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object)
119    {
120        if (object instanceof Content && isInCharge(user, (Content) object))
121        {
122            return getApplicationRights().contains(rightId) ? AccessResult.USER_ALLOWED : AccessResult.UNKNOWN;
123        }
124        
125        return AccessResult.UNKNOWN;
126    }
127
128    public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object)
129    {
130        if (object instanceof Content && isInCharge(user, (Content) object))
131        {
132            return AccessResult.USER_ALLOWED;
133        }
134        
135        return AccessResult.UNKNOWN;
136    }
137
138    /**
139     * If creator, access to a list of rights
140     */
141    public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object)
142    {
143        Map<String, AccessResult> permissionByRight = new HashMap<>();
144        
145        if (isInCharge(user, (Content) object))
146        {
147            for (String rightId : getApplicationRights())
148            {
149                permissionByRight.put(rightId, AccessResult.USER_ALLOWED);
150            }
151        }
152        
153        return permissionByRight;
154    }
155
156    public AccessResult getPermissionForAnonymous(String rightId, Object object)
157    {
158        return AccessResult.UNKNOWN;
159    }
160
161    public AccessResult getReadAccessPermissionForAnonymous(Object object)
162    {
163        return AccessResult.UNKNOWN;
164    }
165
166    public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object)
167    {
168        return AccessResult.UNKNOWN;
169    }
170
171    public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object)
172    {
173        return AccessResult.UNKNOWN;
174    }
175
176    /**
177     * If right requested is in the list, the creator is added the list of USER_ALLOWED
178     */
179    public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object)
180    {
181        Map<UserIdentity, AccessResult> permissionByUser = new HashMap<>();
182        
183        if (getApplicationRights().contains(rightId))
184        {
185            UserIdentity[] personInCharge = getPersonInCharge((Content) object);
186            if (personInCharge != null)
187            {
188                for (UserIdentity userIdentity : personInCharge)
189                {
190                    permissionByUser.put(userIdentity, AccessResult.USER_ALLOWED);
191                }
192            }
193        }
194            
195        return permissionByUser;
196    }
197
198    public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object)
199    {
200        Map<UserIdentity, AccessResult> readPermissionByUser = new HashMap<>();
201        
202        UserIdentity[] personInCharge = getPersonInCharge((Content) object);
203        if (personInCharge != null)
204        {
205            for (UserIdentity userIdentity : personInCharge)
206            {
207                readPermissionByUser.put(userIdentity, AccessResult.USER_ALLOWED);
208            }
209        }
210            
211        return readPermissionByUser;
212    }
213
214    public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object)
215    {
216        return MapUtils.EMPTY_MAP;
217    }
218
219    public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object)
220    {
221        return MapUtils.EMPTY_MAP;
222    }
223
224    public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId)
225    {
226        return false;
227    }
228
229    public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups)
230    {
231        return false;
232    }
233
234    public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId)
235    {
236        return false;
237    }
238
239    public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts)
240    {
241        return false;
242    }
243
244    public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId)
245    {
246        return false;
247    }
248
249    public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts)
250    {
251        return false;
252    }
253    
254    @Override
255    protected AccessExplanation _getAccessExplanation(AccessResult result, Object object, UserIdentity user, Set<GroupIdentity> groups, String rightId)
256    {
257        switch (result)
258        {
259            case USER_ALLOWED:
260            case UNKNOWN:
261                Content jobApplication = (Content) object;
262                ContentValue jobOffer = jobApplication.getValue(JobOfferConstants.JOB_APPLICATION_ATTRIBUTE_PATH_JOB_OFFER);
263                return new AccessExplanation(
264                        getId(),
265                        result,
266                        new I18nizableText("plugin.job-offer", "PLUGINS_JOB_OFFER_APPLICATION_ACCESS_CONTROLLER_" + result.name() + "_EXPLANATION",
267                                Map.of(
268                                        "title", new I18nizableText(jobOffer.getContent().getTitle())
269                                        )
270                                )
271                        );
272            default:
273                return AccessController.getDefaultAccessExplanation(getId(), result);
274        }
275    }
276    
277    @Override
278    protected Iterable< ? extends Object> getHandledObjects(UserIdentity identity, Set<GroupIdentity> groups, Set<Object> workspacesContexts)
279    {
280        String siteName = WebHelper.getSiteName(ContextHelper.getRequest(_context));
281        
282        if (StringUtils.isNotBlank(siteName))
283        {
284            Expression typeExpression = new ContentTypeExpression(Operator.EQ, JobOfferConstants.JOB_OFFER_CONTENT_TYPE);
285            Expression inChargeExpression = new UserExpression(JobOfferConstants.JOB_OFFER_ATTRIBUTE_PATH_PERSON_IN_CHARGE, Operator.EQ, identity, true);
286            Expression siteExpression = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, siteName, ExpressionContext.newInstance().withInternal(true));
287            String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(typeExpression, inChargeExpression, siteExpression));
288            
289            try (AmetysObjectIterable<Content> offers = _resolver.query(query))
290            {
291                if (offers.getSize() > 0)
292                {
293                    List<Expression> applicationsExpression = offers.stream()
294                            .map(Content::getId)
295                            .<Expression>map(id -> new StringExpression(JobOfferConstants.JOB_APPLICATION_ATTRIBUTE_PATH_JOB_OFFER, Operator.EQ, id))
296                            .toList();
297                    
298                    String applicationQuery = ContentQueryHelper.getContentXPathQuery(new AndExpression(
299                            new ContentTypeExpression(Operator.EQ, JobOfferConstants.JOB_APPLICATION_CONTENT_TYPE),
300                            new OrExpression(applicationsExpression)
301                            ));
302                    
303                    return _resolver.query(applicationQuery);
304                }
305            }
306        }
307        return List.of();
308    }
309    
310    @Override
311    protected Collection<String> getHandledRights()
312    {
313        return getApplicationRights();
314    }
315
316    @Override
317    public I18nizableText getObjectCategory(Object object)
318    {
319        return new I18nizableText("plugin.job-offer", "PLUGINS_JOB_OFFER_APPLICATION_ACCESS_CONTROLLER_CONTEXT_CATEGORY");
320    }
321
322    @Override
323    public I18nizableText getObjectLabel(Object object)
324    {
325        if (object instanceof Content application)
326        {
327            ContentValue jobOffer = application.getValue(JobOfferConstants.JOB_APPLICATION_ATTRIBUTE_PATH_JOB_OFFER);
328            return new I18nizableText(jobOffer.getContent().getTitle() + " > " + application.getTitle());
329        }
330        throw new RightsException("Unsupported object: " + object.toString());
331    }
332}