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