001/*
002 *  Copyright 2018 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.cms.model.restrictions;
017
018import java.util.List;
019import java.util.Set;
020
021import org.apache.avalon.framework.component.Component;
022import org.apache.avalon.framework.configuration.Configuration;
023import org.apache.avalon.framework.configuration.ConfigurationException;
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.lang3.StringUtils;
028
029import org.ametys.cms.repository.Content;
030import org.ametys.cms.repository.WorkflowAwareContent;
031import org.ametys.core.right.RightManager;
032import org.ametys.core.right.RightManager.RightResult;
033import org.ametys.core.user.CurrentUserProvider;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.plugins.repository.AmetysRepositoryException;
036import org.ametys.plugins.workflow.support.WorkflowProvider;
037import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
038import org.ametys.runtime.model.ModelItem;
039
040import com.opensymphony.workflow.spi.Step;
041
042/**
043 * Helper for definitions with restrictions on contents 
044 */
045public class ContentRestrictedModelItemHelper implements Component, Serviceable
046{
047    /** The Avalon role name */
048    public static final String ROLE = ContentRestrictedModelItemHelper.class.getName();
049    
050    /** The rights manager. */
051    private RightManager _rightManager;
052    /** Current user provider. */
053    private CurrentUserProvider _currentUserProvider;
054    /** The workflow provider */
055    private WorkflowProvider _workflowProvider;
056    
057    private enum FirstRestrictionsChecksState
058    {
059        /** First checks are OK */
060        TRUE,
061        
062        /** First checks are not OK */
063        FALSE,
064        
065        /** There are more checks to do */
066        UNKNOWN,
067    } 
068    
069    public void service(ServiceManager manager) throws ServiceException
070    {
071        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
072        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
073        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
074    }
075    
076    /**
077     * Determine whether a model item can be read at this time.
078     * @param item the model item on which check the restrictions
079     * @param content The content where item is to be written on. Can be null, on content creation. 
080     * @param restrictions the restrictions to check
081     * @return <code>true</code> if the current user is allowed to write the model item of this content.
082     * @throws AmetysRepositoryException if an error occurs while accessing the content workflow.
083     */
084    @SuppressWarnings("unchecked")
085    public boolean canRead(Content content, ModelItem item, Restrictions restrictions) throws AmetysRepositoryException
086    {
087        FirstRestrictionsChecksState state = _doFirstRestrictionsChecks(content, restrictions, true);
088        if (!FirstRestrictionsChecksState.UNKNOWN.equals(state))
089        {
090            return FirstRestrictionsChecksState.TRUE.equals(state);
091        }
092        
093        ModelItem parent = item.getParent();
094        if (parent != null && parent instanceof RestrictedModelItem)
095        {
096            // Check write access on parent model item
097            return ((RestrictedModelItem<Content>) parent).canRead(content);
098        }
099        
100        return true;
101    }
102
103    /**
104     * Determine whether a model item can be written at this time.
105     * @param item the model item on which check the restrictions
106     * @param content The content where item is to be written on. Can be null, on content creation. 
107     * @param restrictions the restrictions to check
108     * @return <code>true</code> if the current user is allowed to write the model item of this content.
109     * @throws AmetysRepositoryException if an error occurs while accessing the content workflow.
110     */
111    @SuppressWarnings("unchecked")
112    public boolean canWrite(Content content, ModelItem item, Restrictions restrictions) throws AmetysRepositoryException
113    {
114        FirstRestrictionsChecksState state = _doFirstRestrictionsChecks(content, restrictions, false);
115        if (!FirstRestrictionsChecksState.UNKNOWN.equals(state))
116        {
117            return FirstRestrictionsChecksState.TRUE.equals(state);
118        }
119        
120        ModelItem parent = item.getParent();
121        if (parent != null && parent instanceof RestrictedModelItem)
122        {
123            // Check write access on parent model item
124            return ((RestrictedModelItem<Content>) parent).canWrite(content);
125        }
126
127        return canRead(content, item, restrictions);
128    }
129    
130    /**
131     * Does the first checks on restrictions
132     * @param content The content where item is to be read / written on. Can be null, on content creation.
133     * @param restrictions The restrictions to apply
134     * @param forReading <code>true</code> for reading checks, <code>false</code> for writing checks.
135     * @return the state of the first checks
136     * @throws AmetysRepositoryException if an error occurs while accessing the content workflow.
137     */
138    private FirstRestrictionsChecksState _doFirstRestrictionsChecks(Content content, Restrictions restrictions, boolean forReading) throws AmetysRepositoryException
139    {
140        if (restrictions == null)
141        {
142            return FirstRestrictionsChecksState.TRUE;
143        }
144    
145        if (forReading && restrictions.cannotRead() || !forReading && restrictions.cannotWrite())
146        {
147            return FirstRestrictionsChecksState.FALSE;
148        }
149     
150        if (content == null)
151        {
152            // Unable to check right (content is not yet created), assume user has right 
153            return FirstRestrictionsChecksState.TRUE;
154        }
155        
156        boolean hasRights = _hasRights(content, forReading ? restrictions.getReadRightIds() : restrictions.getWriteRightIds());
157        
158        if (!hasRights)
159        {
160            return FirstRestrictionsChecksState.FALSE;
161        }
162        
163        if (content instanceof WorkflowAwareContent)
164        {
165            hasRights = _isInWorkflowStep((WorkflowAwareContent) content, forReading ? restrictions.getReadWorkflowfStepIds() : restrictions.getWriteWorkflowfStepIds());
166            
167            if (!hasRights)
168            {
169                return FirstRestrictionsChecksState.FALSE;
170            }
171        }
172        
173        return FirstRestrictionsChecksState.UNKNOWN;
174    }
175    
176    /**
177     * Check if current user has the given rights.
178     * @param rightLimitations the right limitations.
179     * @param content the content.
180     * @return <code>true</code> if it is on at least one step,
181     *         <code>false</code> otherwise.
182     */
183    private boolean _hasRights(Content content, Set<String> rightLimitations)
184    {
185        if (rightLimitations.isEmpty())
186        {
187            return true;
188        }
189
190        UserIdentity user = _currentUserProvider.getUser();
191
192        for (String rightId : rightLimitations)
193        {
194            if (_rightManager.hasRight(user, rightId, content) == RightResult.RIGHT_ALLOW)
195            {
196                return true;
197            }
198        }
199
200        return false;
201    }
202
203    /**
204     * Check if the workflow of the content is in a given current step.
205     * @param workflowLimitations the workflow limitations.
206     * @param content the content.
207     * @return <code>true</code> if it is on at least one step,
208     *         <code>false</code> otherwise.
209     * @throws AmetysRepositoryException if an error occurs.
210     */
211    private boolean _isInWorkflowStep(WorkflowAwareContent content, Set<Integer> workflowLimitations) throws AmetysRepositoryException
212    {
213        if (workflowLimitations.isEmpty())
214        {
215            return true;
216        }
217        
218        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
219        
220        List<Step> workflowCurrentSteps = workflow.getCurrentSteps(content.getWorkflowId());
221        
222        for (Step step : workflowCurrentSteps)
223        {
224            for (int stepId : workflowLimitations)
225            {
226                if (step.getStepId() == stepId)
227                {
228                    return true;
229                }
230            }
231        }
232
233        // No match
234        return false;
235    }
236    
237    /**
238     * Parses the attribute definition's restrictions.
239     * @param configuration the configuration of the element that have content restrictions
240     * @return the parsed restrictions.
241     * @throws ConfigurationException if the configuration is not valid.
242     */
243    public Restrictions _parseRestrictions(Configuration configuration) throws ConfigurationException
244    {
245        Restrictions restrictions = new Restrictions();
246        _populateRestrictions(configuration, restrictions);
247        return restrictions;
248    }
249    
250    /**
251     * Parses the attribute definition's restrictions.
252     * @param attributeConfiguration the attribute configuration to use.
253     * @param restrictions the restrictions.
254     * @throws ConfigurationException if the configuration is not valid.
255     */
256    private void _populateRestrictions(Configuration attributeConfiguration, Restrictions restrictions) throws ConfigurationException
257    {
258        Configuration restrictToConf = attributeConfiguration.getChild("restrict-to", true);
259
260        _populateNegativeRestrictions(restrictToConf, restrictions);
261        _populateRightRestrictions(restrictToConf, restrictions);
262        _populateWorkflowRestrictions(restrictToConf, restrictions);
263    }
264
265    /**
266     * Populates the negative restrictions.
267     * @param restrictionsConfig the restrictions configuration to use.
268     * @param restrictions the restrictions.
269     * @throws ConfigurationException if the configuration is not valid.
270     */
271    private void _populateNegativeRestrictions(Configuration restrictionsConfig, Restrictions restrictions) throws ConfigurationException
272    {
273        for (Configuration noRightConfig : restrictionsConfig.getChildren("cannot"))
274        {
275            boolean isRead = _parseAccessType(noRightConfig);
276            
277            if (isRead)
278            {
279                restrictions.setCannotRead(true);
280            }
281            else
282            {
283                restrictions.setCannotWrite(true);
284            }
285        }
286    }
287
288    /**
289     * Populates the rights restrictions.
290     * @param restrictionsConfig the restrictions configuration to use.
291     * @param restrictions the restrictions.
292     * @throws ConfigurationException if the configuration is not valid.
293     */
294    private void _populateRightRestrictions(Configuration restrictionsConfig, Restrictions restrictions) throws ConfigurationException
295    {
296        for (Configuration rightConfig : restrictionsConfig.getChildren("right"))
297        {
298            String rightId = rightConfig.getAttribute("id", StringUtils.EMPTY);
299            
300            if (StringUtils.isEmpty(rightId))
301            {
302                throw new ConfigurationException("Attribute 'id' is mandatory on 'right' element in a content type configuration.", rightConfig);
303            }
304
305            boolean isRead = _parseAccessType(rightConfig);
306            
307            if (isRead)
308            {
309                restrictions.addReadRightIds(rightId);
310            }
311            else
312            {
313                restrictions.addWriteRightIds(rightId);
314            }
315        }
316    }
317
318    /**
319     * Populates the workflows restrictions.
320     * @param restrictionsConfig the restrictions configuration to use.
321     * @param restrictions the restrictions.
322     * @throws ConfigurationException if the configuration is not valid.
323     */
324    private void _populateWorkflowRestrictions(Configuration restrictionsConfig, Restrictions restrictions) throws ConfigurationException
325    {
326        for (Configuration workflowConfig : restrictionsConfig.getChildren("workflow"))
327        {
328            String stepId = workflowConfig.getAttribute("step", null);
329            int stepIdValue = -1;
330            
331            if (stepId != null)
332            {
333                try
334                {
335                    stepIdValue = Integer.valueOf(stepId);
336                }
337                catch (NumberFormatException e)
338                {
339                    // Handled just below
340                }
341            }
342            
343            boolean isRead = _parseAccessType(workflowConfig);
344            
345            if (isRead)
346            {
347                restrictions.addReadWorkflowfStepIds(stepIdValue);
348            }
349            else
350            {
351                restrictions.addWriteWorkflowfStepIds(stepIdValue);
352            }
353        }
354    }
355    
356    /**
357     * Parses type attribute from a configuration.
358     * @param configuration the configuration.
359     * @return <code>true</code> for <code>read</code> type,
360     *         <code>false</code> for <code>write</code> type.
361     * @throws ConfigurationException if the configuration is not valid.
362     */
363    private boolean _parseAccessType(Configuration configuration) throws ConfigurationException
364    {
365        String type = configuration.getAttribute("read-write-direction");
366        
367        if ("read".equalsIgnoreCase(type))
368        {
369            return  true;
370        }
371        else if ("write".equalsIgnoreCase(type))
372        {
373            return false;
374        }
375        else
376        {
377            throw new ConfigurationException("Attribute 'type' must be 'read' or 'write'.", configuration);
378        }
379    }
380}