-
Notifications
You must be signed in to change notification settings - Fork 4
SED-4536 granular-access-control-for-resource-types #617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,11 +45,16 @@ | |
| import java.io.InputStream; | ||
| import java.util.*; | ||
| import java.util.function.Consumer; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Path("/resources") | ||
| @Tag(name = "Resources") | ||
| public class ResourceServices extends AbstractStepAsyncServices { | ||
|
|
||
| private static final String RESOURCE_RIGHT_NAME = "resource"; | ||
| public static final String INVALID_ARGUMENTS_A_NON_NULL_RESOURCE_MUST_BE_PROVIDED = "Invalid arguments: a non null resource must be provided."; | ||
| public static final String INVALID_RESOURCE_THE_RESOURCE_HAS_NO_RESOURCE_TYPE_SET = "Invalid resource: the resource has no resourceType set."; | ||
|
|
||
| protected ResourceManager resourceManager; | ||
| private TableService tableService; | ||
|
|
||
|
|
@@ -74,10 +79,25 @@ private void auditLog(String operation, Resource resource) { | |
| AuditLogger.logEntityModification(getSession(), operation, "resources", resource.getId().toHexString(), entityName, attributes); | ||
| } | ||
|
|
||
| private void checkResourceTypeRight(String resourceType, String right) { | ||
| if (resourceType == null || resourceType.isEmpty()) { | ||
| throw new ControllerServiceException(INVALID_RESOURCE_THE_RESOURCE_HAS_NO_RESOURCE_TYPE_SET); | ||
| } | ||
| checkRightIfDefined(RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + resourceType + RIGHT_SEPARATOR + right); | ||
| } | ||
|
|
||
| private void checkResourceTypeRight(Resource resource, String right) { | ||
| //If a resource is null, right is granted too, it's not the role of this function to perform integrity check | ||
| if (resource == null) { | ||
| throw new ControllerServiceException(INVALID_ARGUMENTS_A_NON_NULL_RESOURCE_MUST_BE_PROVIDED); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This exception might be misleading. In some case it might mean that the provided resourceId doesn't exist (61a162b#diff-3e91bc97d77a8308abd81e9cdf8a6c793ab84adaf260c3100ebf11f6959c6e12R165) |
||
| } else { | ||
| checkResourceTypeRight(resource.getResourceType(), right); | ||
| } | ||
| } | ||
|
Comment on lines
+89
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method grants access if a resource has no For improved security, consider a "fail-closed" approach where access is denied for resources without a type, or at least log a warning when such resources are accessed to detect potential issues.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jeromecomte I did this on purpose, because many of our services don't check at all if a resourceType is set or not. If you think it is fine from now on to throw an exception when a resource as no resourceType, I'll make the modification.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @david-stephan This would be safer but we should ensure that we don't break anything. Do you know which clients could call this without resourceType?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well I was hoping you would have an idea :). Now I deep dived into the code, createResource ensure a type is set since 2019 at least. saveResource doesn't but it is apparently only called from the UI which enforce providing a type. The RemoteResourceManager does not use this service, the method saveResource is "Not implemented". The CLI/mvn plugin use the AP services directly. So apart from custom REST clients, no client is supposed to update an existing resource removing the type. |
||
|
|
||
| @POST | ||
| @Path("/content") | ||
| @Secured(right = "resource-write") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + WRITE_RIGHT) | ||
| @Consumes(MediaType.MULTIPART_FORM_DATA) | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| public ResourceUploadResponse createResource(@FormDataParam("file") InputStream uploadedInputStream, | ||
|
|
@@ -91,9 +111,10 @@ public ResourceUploadResponse createResource(@FormDataParam("file") InputStream | |
|
|
||
| if (uploadedInputStream == null || fileDetail == null) | ||
| throw new RuntimeException("Invalid arguments"); | ||
| if (resourceType == null || resourceType.length() == 0) | ||
| if (resourceType == null || resourceType.isEmpty()) | ||
| throw new RuntimeException("Missing resource type query parameter 'type'"); | ||
|
|
||
| checkResourceTypeRight(resourceType, WRITE_RIGHT); | ||
| try { | ||
| Resource resource = resourceManager.createTrackedResource( | ||
| resourceType, isDirectory, uploadedInputStream, fileDetail.getFileName(), objectEnricher, | ||
|
|
@@ -113,24 +134,35 @@ private ControllerServiceException uploadFileNotAnArchive() { | |
| } | ||
|
|
||
| @POST | ||
| @Secured(right = "resource-write") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + WRITE_RIGHT) | ||
| @Consumes(MediaType.APPLICATION_JSON) | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| public Resource saveResource(Resource resource) throws IOException { | ||
| // Always check that we have write access to the new resource type, this automatically ensure the resource and its resourceType are not null | ||
| checkResourceTypeRight(resource, WRITE_RIGHT); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation has a security vulnerability. A user could update a resource and remove its Additionally, for consistency with if (resource.getId() != null) {
// This is an update, check against the stored resource
Resource storedResource = resourceManager.getResource(resource.getId().toHexString());
checkResourceTypeRight(storedResource, WRITE_RIGHT);
} else {
// This is a new resource, type is mandatory
if (resource.getResourceType() == null || resource.getResourceType().isEmpty()) {
throw new RuntimeException("Missing resourceType for new resource");
}
}
checkResourceTypeRight(resource, WRITE_RIGHT);
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking now access to both types (current stored in DB and new one), also ensure types is set and resource is not null |
||
| // When updating a resource, we need write access to both the original and new types | ||
| String resourceId = resource.getId().toHexString(); | ||
| if (resourceManager.resourceExists(resourceId)) { | ||
| String originalResourceType = resourceManager.getResource(resourceId).getResourceType(); | ||
| if (!resource.getResourceType().equals(originalResourceType)) { | ||
| checkResourceTypeRight(originalResourceType, WRITE_RIGHT); | ||
| } | ||
| } | ||
| auditLog("save", resource); | ||
| return resourceManager.saveResource(resource); | ||
| } | ||
|
|
||
| @POST | ||
| @Path("/{id}/content") | ||
| @Secured(right = "resource-write") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + WRITE_RIGHT) | ||
| @Consumes(MediaType.MULTIPART_FORM_DATA) | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| public ResourceUploadResponse saveResourceContent(@PathParam("id") String resourceId, @FormDataParam("file") InputStream uploadedInputStream, | ||
| @FormDataParam("file") FormDataContentDisposition fileDetail) throws Exception { | ||
| if (uploadedInputStream == null || fileDetail == null) | ||
| throw new RuntimeException("Invalid arguments"); | ||
|
|
||
| checkResourceTypeRight(resourceManager.getResource(resourceId), WRITE_RIGHT); | ||
| try { | ||
| Resource resource = resourceManager.saveResourceContent(resourceId, uploadedInputStream, fileDetail.getFileName(), null, getSession().getUser().getUsername()); | ||
| auditLog("save-content", resource); | ||
|
|
@@ -143,31 +175,35 @@ public ResourceUploadResponse saveResourceContent(@PathParam("id") String resour | |
| @GET | ||
| @Secured | ||
| @Path("/{id}") | ||
| @Secured(right = "resource-read") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + READ_RIGHT) | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| public Resource getResource(@PathParam("id") String resourceId) throws IOException { | ||
| try { | ||
| return resourceManager.getResource(resourceId); | ||
| Resource resource = resourceManager.getResource(resourceId); | ||
| checkResourceTypeRight(resource, READ_RIGHT); | ||
| return resource; | ||
| } catch (ResourceMissingException e) { | ||
| throw new ControllerServiceException(404, e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @GET | ||
| @Path("/{id}/content") | ||
| @Secured(right = "resource-read") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + READ_RIGHT) | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| public Response getResourceContent(@PathParam("id") String resourceId, @QueryParam("inline") boolean inline) throws IOException { | ||
| ResourceRevisionContent resourceContent = resourceManager.getResourceContent(resourceId); | ||
| checkResourceTypeRight(resourceContent.getResource(), READ_RIGHT); | ||
| return getResponseForResourceRevisionContent(resourceContent, inline); | ||
| } | ||
|
|
||
| @DELETE | ||
| @Secured | ||
| @Path("/{id}") | ||
| @Secured(right = "resource-delete") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + DELETE_RIGHT) | ||
| public void deleteResource(@PathParam("id") String resourceId) { | ||
| Resource resource = resourceManager.getResource(resourceId); | ||
| checkResourceTypeRight(resource, DELETE_RIGHT); | ||
| assertEntityIsEditableInContext(resource); | ||
| auditLog("delete", resource); | ||
| resourceManager.deleteResource(resourceId); | ||
|
|
@@ -176,9 +212,10 @@ public void deleteResource(@PathParam("id") String resourceId) { | |
| @DELETE | ||
| @Secured | ||
| @Path("/{id}/revisions") | ||
| @Secured(right = "resource-delete") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + DELETE_RIGHT) | ||
| public void deleteResourceRevisions(@PathParam("id") String resourceId) { | ||
| Resource resource = resourceManager.getResource(resourceId); | ||
| checkResourceTypeRight(resource, DELETE_RIGHT); | ||
| assertEntityIsEditableInContext(resource); | ||
| auditLog("delete-revisions", resource); | ||
| resourceManager.deleteResourceRevisionContent(resourceId); | ||
|
|
@@ -207,19 +244,28 @@ public AsyncTaskStatus<TableBulkOperationReport> bulkDelete(TableBulkOperationRe | |
| @GET | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| @Path("/revision/{id}/content") | ||
| @Secured(right = "resource-read") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + READ_RIGHT) | ||
| public Response getResourceRevisionContent(@PathParam("id") String resourceRevisionId, @QueryParam("inline") boolean inline) throws IOException { | ||
| ResourceRevisionContentImpl resourceContent = resourceManager.getResourceRevisionContent(resourceRevisionId); | ||
| checkResourceTypeRight(resourceContent.getResource(), READ_RIGHT); | ||
| return getResponseForResourceRevisionContent(resourceContent, inline); | ||
| } | ||
|
|
||
| @POST | ||
| @Path("/find") | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| @Consumes(MediaType.APPLICATION_JSON) | ||
| @Secured(right = "resource-read") | ||
| @Secured(right = RESOURCE_RIGHT_NAME + RIGHT_SEPARATOR + READ_RIGHT) | ||
| public List<Resource> findManyByCriteria(Map<String, String> criteria) { | ||
| return resourceManager.findManyByCriteria(criteria); | ||
| return resourceManager.findManyByCriteria(criteria).stream().filter(r -> { | ||
| try { | ||
| checkResourceTypeRight(r, READ_RIGHT); | ||
| return true; | ||
| } catch (Exception e) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| //we filter out the resources for which the user has no access | ||
| return false; | ||
| } | ||
| }).collect(Collectors.toList()); | ||
| } | ||
|
|
||
| protected Response getResponseForResourceRevisionContent(ResourceRevisionContent resourceContent, boolean inline) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment doesn't seem to match the implementation