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