001/* 002 * Copyright 2024 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.plugins.extraction.rights; 017 018import java.io.IOException; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.stream.Stream; 025 026import org.apache.avalon.framework.activity.Initializable; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.commons.collections.MapUtils; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.commons.lang3.Strings; 033import org.apache.excalibur.source.SourceException; 034import org.apache.excalibur.source.SourceResolver; 035import org.apache.excalibur.source.impl.FileSource; 036 037import org.ametys.cms.contenttype.ContentTypesHelper; 038import org.ametys.cms.repository.Content; 039import org.ametys.core.group.GroupIdentity; 040import org.ametys.core.right.AccessController; 041import org.ametys.core.right.AccessExplanation; 042import org.ametys.core.right.RightsException; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.plugins.core.impl.right.AbstractRightBasedAccessController; 045import org.ametys.plugins.extraction.ExtractionConstants; 046import org.ametys.plugins.extraction.execution.Extraction; 047import org.ametys.plugins.extraction.execution.ExtractionDAO; 048import org.ametys.runtime.i18n.I18nizableText; 049 050 051/** 052 * {@link AccessController} to allow read access and handle for author of a extraction file 053 * 054 */ 055public class ExtractionAuthorAccessController extends AbstractRightBasedAccessController implements Serviceable, Initializable 056{ 057 private static final List<String> __AUTHOR_RIGHTS = List.of(ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID, "Workflow_Rights_Edition_Online"); 058 059 private SourceResolver _srcResolver; 060 private String _rootPath; 061 private ExtractionDAO _extractionDAO; 062 private ContentTypesHelper _contentTypesHelper; 063 064 public void service(ServiceManager manager) throws ServiceException 065 { 066 _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 067 _extractionDAO = (ExtractionDAO) manager.lookup(ExtractionDAO.ROLE); 068 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 069 } 070 071 public void initialize() throws Exception 072 { 073 FileSource rootDir = (FileSource) _srcResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR); 074 // use the URI. The path is not available if the definitions folder is not created at start time 075 _rootPath = rootDir.getURI(); 076 } 077 078 public boolean supports(Object object) 079 { 080 return object instanceof Extraction 081 // a fileSource that don't exist is not a collection. 082 // not checking that it exists leads to the root being supported if not created 083 || object instanceof FileSource fileSource && fileSource.exists() && !fileSource.isCollection() && fileSource.getURI().startsWith(_rootPath) 084 || object instanceof Content content && _contentTypesHelper.isInstanceOf(content, ExtractionConstants.DESCRIPTION_CONTENT_TYPE_ID); 085 } 086 087 public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object) 088 { 089 if (user.equals(_getAuthor(object))) 090 { 091 return __AUTHOR_RIGHTS.contains(rightId) ? AccessResult.USER_ALLOWED : AccessResult.UNKNOWN; 092 } 093 094 return AccessResult.UNKNOWN; 095 } 096 097 public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object) 098 { 099 return user.equals(_getAuthor(object)) ? AccessResult.USER_ALLOWED : AccessResult.UNKNOWN; 100 } 101 102 /** 103 * If creator, access to a list of rights 104 */ 105 public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object) 106 { 107 Map<String, AccessResult> permissionByRight = new HashMap<>(); 108 109 if (user.equals(_getAuthor(object))) 110 { 111 for (String rightId : __AUTHOR_RIGHTS) 112 { 113 permissionByRight.put(rightId, AccessResult.USER_ALLOWED); 114 } 115 } 116 117 return permissionByRight; 118 } 119 120 public AccessResult getPermissionForAnonymous(String rightId, Object object) 121 { 122 return AccessResult.UNKNOWN; 123 } 124 125 public AccessResult getReadAccessPermissionForAnonymous(Object object) 126 { 127 return AccessResult.UNKNOWN; 128 } 129 130 public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object) 131 { 132 return AccessResult.UNKNOWN; 133 } 134 135 public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object) 136 { 137 return AccessResult.UNKNOWN; 138 } 139 140 /** 141 * If right requested is in the list, the creator is added the list of USER_ALLOWED 142 */ 143 public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object) 144 { 145 Map<UserIdentity, AccessResult> permissionByUser = new HashMap<>(); 146 147 if (__AUTHOR_RIGHTS.contains(rightId)) 148 { 149 UserIdentity extractionAuthor = _getAuthor(object); 150 permissionByUser.put(extractionAuthor, AccessResult.USER_ALLOWED); 151 } 152 return permissionByUser; 153 } 154 155 public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object) 156 { 157 return MapUtils.EMPTY_MAP; 158 } 159 160 public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object) 161 { 162 return MapUtils.EMPTY_MAP; 163 } 164 165 public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object) 166 { 167 return MapUtils.EMPTY_MAP; 168 } 169 170 public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId) 171 { 172 return false; 173 } 174 175 public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups) 176 { 177 return false; 178 } 179 180 public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId) 181 { 182 return false; 183 } 184 185 public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts) 186 { 187 return false; 188 } 189 190 public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId) 191 { 192 return false; 193 } 194 195 public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts) 196 { 197 return false; 198 } 199 200 @Override 201 protected AccessExplanation _getAccessExplanation(AccessResult result, Object object, UserIdentity user, Set<GroupIdentity> groups, String rightId) 202 { 203 switch (result) 204 { 205 case USER_ALLOWED: 206 case UNKNOWN: 207 if (object instanceof Content) 208 { 209 return new AccessExplanation( 210 getId(), 211 result, 212 new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_CONTENT_AUTHOR_ACCESS_CONTROLLER_" + result.name() + "_EXPLANATION", 213 Map.of("title", getObjectLabel(object))) 214 ); 215 } 216 else 217 { 218 return new AccessExplanation( 219 getId(), 220 result, 221 new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_AUTHOR_ACCESS_CONTROLLER_" + result.name() + "_EXPLANATION", 222 Map.of("title", getObjectLabel(object))) 223 ); 224 } 225 default: 226 return AccessController.getDefaultAccessExplanation(getId(), result); 227 } 228 } 229 230 private UserIdentity _getAuthor(Object object) 231 { 232 if (object instanceof Extraction extraction) 233 { 234 return extraction.getAuthor(); 235 } 236 else if (object instanceof FileSource fileSource) 237 { 238 return _extractionDAO.getAuthor(fileSource); 239 } 240 else if (object instanceof Content content) 241 { 242 return content.getCreator(); 243 } 244 return null; 245 } 246 247 private String _getExtractionName(Object object) 248 { 249 if (object instanceof FileSource fileSource) 250 { 251 String target = StringUtils.substringAfter(_extractionDAO.getExtractionRightPath(fileSource), ExtractionAccessController.ROOT_CONTEXT); 252 target = Strings.CS.replace(target.substring(1), "/", " > "); 253 return target; 254 } 255 // We can't include the hierarchy from the extraction or content 256 // But it shouldn't be visible as there is nothing display the explanation for it 257 else if (object instanceof Extraction extraction) 258 { 259 return extraction.getFileName(); 260 } 261 else if (object instanceof Content content) 262 { 263 return content.getTitle(); 264 } 265 return null; 266 } 267 268 public I18nizableText getObjectLabel(Object object) 269 { 270 String extractionName = _getExtractionName(object); 271 if (extractionName != null) 272 { 273 return new I18nizableText(extractionName); 274 } 275 throw new RightsException("Unsupported context: " + object.toString()); 276 } 277 278 public I18nizableText getObjectCategory(Object object) 279 { 280 return ExtractionAccessController.EXTRACTION_CONTEXT_CATEGORY; 281 } 282 283 @Override 284 protected Iterable< ? extends Object> getHandledObjects(UserIdentity identity, Set<GroupIdentity> groups, Set<Object> workspacesContexts) 285 { 286 if (workspacesContexts.contains("/cms")) 287 { 288 try 289 { 290 FileSource rootDir = (FileSource) _srcResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR); 291 if (rootDir.getFile().exists()) 292 { 293 Stream<FileSource> definitions = _getDefinitions(rootDir); 294 return definitions.toList(); 295 } 296 } 297 catch (IOException e) 298 { 299 getLogger().warn("Failed to compute the list of extractions"); 300 } 301 } 302 303 return List.of(); 304 } 305 306 private Stream<FileSource> _getDefinitions(FileSource source) 307 { 308 if (source.isCollection()) 309 { 310 try 311 { 312 return source.getChildren().stream() 313 .filter(FileSource.class::isInstance) 314 .flatMap(src -> _getDefinitions((FileSource) src)); 315 } 316 catch (SourceException e) 317 { 318 getLogger().warn("Failed to compute the list of extractions"); 319 return Stream.of(); 320 } 321 } 322 else 323 { 324 return Stream.of(source); 325 } 326 } 327 328 public ExplanationObject getExplanationObject(Object object) 329 { 330 if (object instanceof FileSource source) 331 { 332 return new ExplanationObject( 333 // we convert the source to the right path to be able to merge with ExtractionAccessController 334 _extractionDAO.getExtractionRightPath(source), 335 getObjectLabel(object), 336 getObjectCategory(object) 337 ); 338 } 339 return super.getExplanationObject(object); 340 } 341 342 @Override 343 protected Collection<String> getHandledRights() 344 { 345 return __AUTHOR_RIGHTS; 346 } 347}