001/*
002 *  Copyright 2025 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.HashSet;
019import java.util.List;
020import java.util.Set;
021
022import org.apache.avalon.framework.component.Component;
023import org.apache.avalon.framework.configuration.Configurable;
024import org.apache.avalon.framework.configuration.Configuration;
025import org.apache.avalon.framework.configuration.ConfigurationException;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.cms.repository.Content;
032import org.ametys.cms.repository.WorkflowAwareContent;
033import org.ametys.core.right.RightManager;
034import org.ametys.core.right.RightManager.RightResult;
035import org.ametys.core.user.CurrentUserProvider;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.plugins.repository.AmetysRepositoryException;
038import org.ametys.plugins.workflow.support.WorkflowProvider;
039import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
040import org.ametys.runtime.plugin.component.AbstractLogEnabled;
041
042import com.opensymphony.workflow.spi.Step;
043
044/**
045 * Default implementation for restrictions on content attributes.<br/>
046 * Restrictions can be on read or write direction, based on rights and/or workflow step ids, according its XML configuration:
047 * <pre>
048 *  &lt;cannot read-write-direction="read|write"/>
049 *  &lt;right read-write-direction="read|write" id="RightId"/>
050 *  &lt;workflow read-write-direction="read|write" step="3"/>
051 * </pre>
052 */
053public class DefaultRestriction extends AbstractLogEnabled implements Restriction, Configurable, Serviceable, Component
054{
055    /** The rights manager. */
056    protected RightManager _rightManager;
057    /** Current user provider. */
058    protected CurrentUserProvider _currentUserProvider;
059    /** The workflow provider */
060    protected WorkflowProvider _workflowProvider;
061    
062    /** Read workflow step ids. */
063    protected Set<String> _writeRightIds = new HashSet<>();
064    /** Cannot read status. */
065    private boolean _cannotRead;
066    /** Cannot write status. */
067    private boolean _cannotWrite;
068    /** Read right ids. */
069    private Set<String> _readRightIds = new HashSet<>();
070    /** Write right ids. */
071    private Set<Integer> _readWorkflowfStepIds = new HashSet<>();
072    /** Write workflow step ids. */
073    private Set<Integer> _writeWorkflowfStepIds = new HashSet<>();
074    
075    public void configure(Configuration configuration) throws ConfigurationException
076    {
077        _populateNegativeRestrictions(configuration);
078        _populateRightRestrictions(configuration);
079        _populateWorkflowRestrictions(configuration);
080    }
081
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
085        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
086        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
087    }
088    
089    public RestrictionResult canRead(Content content, RestrictedModelItem modelItem)
090    {
091        return _doRestrictionsChecks(content, modelItem, true);
092    }
093
094    public RestrictionResult canWrite(Content content, RestrictedModelItem modelItem)
095    {
096        return _doRestrictionsChecks(content, modelItem, false);
097    }
098    
099    private RestrictionResult _doRestrictionsChecks(Content content, RestrictedModelItem modelItem, boolean forReading) throws AmetysRepositoryException
100    {
101        if (forReading && _cannotRead || !forReading && _cannotWrite)
102        {
103            return RestrictionResult.FALSE;
104        }
105     
106        if (content == null)
107        {
108            // Unable to check right (content is not yet created), assume user has right
109            return RestrictionResult.TRUE;
110        }
111        
112        boolean hasRights = _hasRights(content, modelItem, forReading ? _readRightIds : _writeRightIds);
113        
114        if (!hasRights)
115        {
116            return RestrictionResult.FALSE;
117        }
118        
119        if (content instanceof WorkflowAwareContent)
120        {
121            hasRights = _isInWorkflowStep((WorkflowAwareContent) content, forReading ? _readWorkflowfStepIds : _writeWorkflowfStepIds);
122            
123            if (!hasRights)
124            {
125                return RestrictionResult.FALSE;
126            }
127        }
128        
129        return RestrictionResult.UNKNOWN;
130    }
131    
132    /**
133     * Check if current user has the given rights.
134     * @param content the content.
135     * @param modelItem the model item on which check the restrictions
136     * @param rightLimitations the right limitations.
137     * @return <code>true</code> if user has at least one right,
138     *         <code>false</code> otherwise.
139     */
140    protected boolean _hasRights(Content content, RestrictedModelItem modelItem, Set<String> rightLimitations)
141    {
142        if (rightLimitations.isEmpty())
143        {
144            return true;
145        }
146
147        UserIdentity user = _currentUserProvider.getUser();
148
149        for (String rightId : rightLimitations)
150        {
151            if (_rightManager.hasRight(user, rightId, content) == RightResult.RIGHT_ALLOW)
152            {
153                return true;
154            }
155        }
156
157        return false;
158    }
159    
160    /**
161     * Check if content is on given workflow steps
162     * @param content the content.
163     * @param workflowLimitations the workflow step ids
164     * @return <code>true</code> if it is on at least one step,
165     *         <code>false</code> otherwise.
166     * @throws AmetysRepositoryException if failed to get content's workflow current step
167     */
168    protected boolean _isInWorkflowStep(WorkflowAwareContent content, Set<Integer> workflowLimitations) throws AmetysRepositoryException
169    {
170        if (workflowLimitations.isEmpty())
171        {
172            return true;
173        }
174        
175        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
176        
177        List<Step> workflowCurrentSteps = workflow.getCurrentSteps(content.getWorkflowId());
178        
179        for (Step step : workflowCurrentSteps)
180        {
181            for (int stepId : workflowLimitations)
182            {
183                if (step.getStepId() == stepId)
184                {
185                    return true;
186                }
187            }
188        }
189
190        // No match
191        return false;
192    }
193    
194    /**
195     * Populates the negative restrictions.
196     * @param restrictionsConfig the restrictions configuration to use.
197     * @throws ConfigurationException if the configuration is not valid.
198     */
199    private void _populateNegativeRestrictions(Configuration restrictionsConfig) throws ConfigurationException
200    {
201        for (Configuration noRightConfig : restrictionsConfig.getChildren("cannot"))
202        {
203            boolean isRead = _parseAccessType(noRightConfig);
204            
205            if (isRead)
206            {
207                _cannotRead = true;
208            }
209            else
210            {
211                _cannotWrite = true;
212            }
213        }
214    }
215
216    /**
217     * Populates the rights restrictions.
218     * @param restrictionsConfig the restrictions configuration to use.
219     * @throws ConfigurationException if the configuration is not valid.
220     */
221    private void _populateRightRestrictions(Configuration restrictionsConfig) throws ConfigurationException
222    {
223        for (Configuration rightConfig : restrictionsConfig.getChildren("right"))
224        {
225            String rightId = rightConfig.getAttribute("id", StringUtils.EMPTY);
226            
227            if (StringUtils.isEmpty(rightId))
228            {
229                throw new ConfigurationException("Attribute 'id' is mandatory on 'right' element in a content type configuration.", rightConfig);
230            }
231
232            boolean isRead = _parseAccessType(rightConfig);
233            
234            if (isRead)
235            {
236                _readRightIds.add(rightId);
237            }
238            else
239            {
240                _writeRightIds.add(rightId);
241            }
242        }
243    }
244
245    /**
246     * Populates the workflows restrictions.
247     * @param restrictionsConfig the restrictions configuration to use.
248     * @throws ConfigurationException if the configuration is not valid.
249     */
250    private void _populateWorkflowRestrictions(Configuration restrictionsConfig) throws ConfigurationException
251    {
252        for (Configuration workflowConfig : restrictionsConfig.getChildren("workflow"))
253        {
254            String stepId = workflowConfig.getAttribute("step", null);
255            int stepIdValue = -1;
256            
257            if (stepId != null)
258            {
259                try
260                {
261                    stepIdValue = Integer.valueOf(stepId);
262                }
263                catch (NumberFormatException e)
264                {
265                    // Handled just below
266                }
267            }
268            
269            boolean isRead = _parseAccessType(workflowConfig);
270            
271            if (isRead)
272            {
273                _readWorkflowfStepIds.add(stepIdValue);
274            }
275            else
276            {
277                _writeWorkflowfStepIds.add(stepIdValue);
278            }
279        }
280    }
281    
282    /**
283     * Parses type attribute from a configuration.
284     * @param configuration the configuration.
285     * @return <code>true</code> for <code>read</code> type,
286     *         <code>false</code> for <code>write</code> type.
287     * @throws ConfigurationException if the configuration is not valid.
288     */
289    private boolean _parseAccessType(Configuration configuration) throws ConfigurationException
290    {
291        String type = configuration.getAttribute("read-write-direction");
292        
293        if ("read".equalsIgnoreCase(type))
294        {
295            return  true;
296        }
297        else if ("write".equalsIgnoreCase(type))
298        {
299            return false;
300        }
301        else
302        {
303            throw new ConfigurationException("Attribute 'type' must be 'read' or 'write'.", configuration);
304        }
305    }
306
307}