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 * <cannot read-write-direction="read|write"/> 049 * <right read-write-direction="read|write" id="RightId"/> 050 * <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}