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