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