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 /** 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}