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