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    /** Cannot read status. */
063    private boolean _cannotRead;
064    /** Cannot write status. */
065    private boolean _cannotWrite;
066    /** Read right ids. */
067    private Set<String> _readRightIds = new HashSet<>();
068    /** Read workflow step ids. */
069    private Set<String> _writeRightIds = 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, true);
092    }
093
094    public RestrictionResult canWrite(Content content, RestrictedModelItem modelItem)
095    {
096        return _doRestrictionsChecks(content, false);
097    }
098    
099    private RestrictionResult _doRestrictionsChecks(Content content, 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, 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 rightLimitations the right limitations.
136     * @return <code>true</code> if user has at least one right,
137     *         <code>false</code> otherwise.
138     */
139    protected boolean _hasRights(Content content, Set<String> rightLimitations)
140    {
141        if (rightLimitations.isEmpty())
142        {
143            return true;
144        }
145
146        UserIdentity user = _currentUserProvider.getUser();
147
148        for (String rightId : rightLimitations)
149        {
150            if (_rightManager.hasRight(user, rightId, content) == RightResult.RIGHT_ALLOW)
151            {
152                return true;
153            }
154        }
155
156        return false;
157    }
158    
159    /**
160     * Check if content is on given workflow steps
161     * @param content the content.
162     * @param workflowLimitations the workflow step ids
163     * @return <code>true</code> if it is on at least one step,
164     *         <code>false</code> otherwise.
165     * @throws AmetysRepositoryException if failed to get content's workflow current step
166     */
167    protected boolean _isInWorkflowStep(WorkflowAwareContent content, Set<Integer> workflowLimitations) throws AmetysRepositoryException
168    {
169        if (workflowLimitations.isEmpty())
170        {
171            return true;
172        }
173        
174        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
175        
176        List<Step> workflowCurrentSteps = workflow.getCurrentSteps(content.getWorkflowId());
177        
178        for (Step step : workflowCurrentSteps)
179        {
180            for (int stepId : workflowLimitations)
181            {
182                if (step.getStepId() == stepId)
183                {
184                    return true;
185                }
186            }
187        }
188
189        // No match
190        return false;
191    }
192    
193    /**
194     * Populates the negative restrictions.
195     * @param restrictionsConfig the restrictions configuration to use.
196     * @throws ConfigurationException if the configuration is not valid.
197     */
198    private void _populateNegativeRestrictions(Configuration restrictionsConfig) throws ConfigurationException
199    {
200        for (Configuration noRightConfig : restrictionsConfig.getChildren("cannot"))
201        {
202            boolean isRead = _parseAccessType(noRightConfig);
203            
204            if (isRead)
205            {
206                _cannotRead = true;
207            }
208            else
209            {
210                _cannotWrite = true;
211            }
212        }
213    }
214
215    /**
216     * Populates the rights restrictions.
217     * @param restrictionsConfig the restrictions configuration to use.
218     * @throws ConfigurationException if the configuration is not valid.
219     */
220    private void _populateRightRestrictions(Configuration restrictionsConfig) throws ConfigurationException
221    {
222        for (Configuration rightConfig : restrictionsConfig.getChildren("right"))
223        {
224            String rightId = rightConfig.getAttribute("id", StringUtils.EMPTY);
225            
226            if (StringUtils.isEmpty(rightId))
227            {
228                throw new ConfigurationException("Attribute 'id' is mandatory on 'right' element in a content type configuration.", rightConfig);
229            }
230
231            boolean isRead = _parseAccessType(rightConfig);
232            
233            if (isRead)
234            {
235                _readRightIds.add(rightId);
236            }
237            else
238            {
239                _writeRightIds.add(rightId);
240            }
241        }
242    }
243
244    /**
245     * Populates the workflows restrictions.
246     * @param restrictionsConfig the restrictions configuration to use.
247     * @throws ConfigurationException if the configuration is not valid.
248     */
249    private void _populateWorkflowRestrictions(Configuration restrictionsConfig) throws ConfigurationException
250    {
251        for (Configuration workflowConfig : restrictionsConfig.getChildren("workflow"))
252        {
253            String stepId = workflowConfig.getAttribute("step", null);
254            int stepIdValue = -1;
255            
256            if (stepId != null)
257            {
258                try
259                {
260                    stepIdValue = Integer.valueOf(stepId);
261                }
262                catch (NumberFormatException e)
263                {
264                    // Handled just below
265                }
266            }
267            
268            boolean isRead = _parseAccessType(workflowConfig);
269            
270            if (isRead)
271            {
272                _readWorkflowfStepIds.add(stepIdValue);
273            }
274            else
275            {
276                _writeWorkflowfStepIds.add(stepIdValue);
277            }
278        }
279    }
280    
281    /**
282     * Parses type attribute from a configuration.
283     * @param configuration the configuration.
284     * @return <code>true</code> for <code>read</code> type,
285     *         <code>false</code> for <code>write</code> type.
286     * @throws ConfigurationException if the configuration is not valid.
287     */
288    private boolean _parseAccessType(Configuration configuration) throws ConfigurationException
289    {
290        String type = configuration.getAttribute("read-write-direction");
291        
292        if ("read".equalsIgnoreCase(type))
293        {
294            return  true;
295        }
296        else if ("write".equalsIgnoreCase(type))
297        {
298            return false;
299        }
300        else
301        {
302            throw new ConfigurationException("Attribute 'type' must be 'read' or 'write'.", configuration);
303        }
304    }
305
306}