diff --git a/appinfo/routes.php b/appinfo/routes.php index 31455ab..022fc5a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -8,6 +8,7 @@ return [ ['name' => 'resource#show', 'url' => '/resources/{resourceId}', 'verb' => 'GET'], ['name' => 'resource#create', 'url' => '/resources/{resourceId}', 'verb' => 'POST'], ['name' => 'resource#update', 'url' => '/resources/{resourceId}', 'verb' => 'PUT'], + ['name' => 'resource#subResources', 'url' => '/resources/{resourceId}/subResources', 'verb' => 'GET'], ['name' => 'resource_member#index', 'url' => '/resources/{resourceId}/members', 'verb' => 'GET'], ['name' => 'resource_member#create', 'url' => '/resources/{resourceId}/members', 'verb' => 'POST'], ['name' => 'resource_member#update', 'url' => '/resources/members/{id}', 'verb' => 'PUT'], diff --git a/lib/Controller/BaseController.php b/lib/Controller/BaseController.php index 7712bd8..14f4984 100644 --- a/lib/Controller/BaseController.php +++ b/lib/Controller/BaseController.php @@ -9,7 +9,7 @@ use OCP\AppFramework\Controller; use OCP\IRequest; class BaseController extends Controller { - private AuthorizationService $authorizationService; + protected AuthorizationService $authorizationService; public function __construct( ) { diff --git a/lib/Controller/ResourceController.php b/lib/Controller/ResourceController.php index 2b68dea..83ca594 100644 --- a/lib/Controller/ResourceController.php +++ b/lib/Controller/ResourceController.php @@ -8,6 +8,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCA\OrganizationFolders\Db\Resource; use OCA\OrganizationFolders\Service\ResourceService; use OCA\OrganizationFolders\Service\ResourceMemberService; +use OCA\OrganizationFolders\Service\OrganizationFolderService; use OCA\OrganizationFolders\Traits\ApiObjectController; class ResourceController extends BaseController { @@ -15,10 +16,12 @@ class ResourceController extends BaseController { use ApiObjectController; public const MEMBERS_INCLUDE = 'members'; + public const SUBRESOURCES_INCLUDE = 'subresources'; public function __construct( private ResourceService $service, private ResourceMemberService $memberService, + private OrganizationFolderService $organizationFolderService, private string $userId, ) { parent::__construct(); @@ -37,6 +40,10 @@ class ResourceController extends BaseController { $result["members"] = $this->memberService->findAll($resource->getId()); } + if($this->shouldInclude(self::SUBRESOURCES_INCLUDE, $includes)) { + $result["subresources"] = $this->getSubResources($resource->getId()); + } + return $result; } @@ -68,16 +75,18 @@ class ResourceController extends BaseController { ?string $include, ): JSONResponse { return $this->handleErrors(function () use ($organizationFolderId, $type, $name, $parentResourceId, $active, $inheritManagers, $membersAclPermission, $managersAclPermission, $inheritedAclPermission, $include) { + $organizationFolder = $this->organizationFolderService->find($organizationFolderId); + if(!is_null($parentResourceId)) { $parentResource = $this->service->find($parentResourceId); $this->denyAccessUnlessGranted(['CREATE_SUBRESOURCE'], $parentResource); } else { - // TODO: ask future organization folder voter + $this->denyAccessUnlessGranted(['CREATE_RESOURCE'], $organizationFolder); } $resource = $this->service->create( - organizationFolderId: $organizationFolderId, + organizationFolderId: $organizationFolder->getId(), type: $type, name: $name, parentResourceId: $parentResourceId, @@ -126,4 +135,44 @@ class ResourceController extends BaseController { return $this->getApiObjectFromEntity($resource, $include); }); } + + + + #[NoAdminRequired] + public function subResources(int $resourceId): JSONResponse { + return $this->handleNotFound(function () use ($resourceId) { + return $this->getSubResources($resourceId); + }); + } + + protected function getSubResources(int $resourceId): array { + $resource = $this->service->find($resourceId); + $organizationFolder = $this->organizationFolderService->find($resource->getOrganizationFolderId()); + + $subresources = $this->service->getSubResources($resource); + + $result = []; + + if($this->authorizationService->isGranted(['MANAGE_ALL_RESOURCES'], $organizationFolder)) { + /* fastpath: access to all subresources */ + $result = $subresources; + } else { + foreach($subresources as $subresource) { + // Future optimization potential 1: the following will potentially check the permissions of each of these subresources all the way up the resource tree. + // As sibling resources these subresources share the same resources above them in the tree. + // So if access to the parent resource is granted, all subresources with inheritManagers can be granted immediately. + // For all other subresources only a check if user has direct (non-inherited) manager rights is neccessary. + + // Future optimization potential 2: READ permission check checks MANAGE_ALL_RESOURCES again, at this point we know this to be false, because of the fastpath. + // Could be replaced with something like a READ_DIRECT (name TBD) permission check, which does not check this again. + if($this->authorizationService->isGranted(['READ'], $resource)) { + $result[] = $subresource; + } else if($this->authorizationService->isGranted(['READ_LIMITED'], $resource)) { + $result[] = $subresource->limitedJsonSerialize(); + } + } + } + + return $result; + } } \ No newline at end of file diff --git a/lib/Db/Resource.php b/lib/Db/Resource.php index 5304561..c07c05c 100644 --- a/lib/Db/Resource.php +++ b/lib/Db/Resource.php @@ -24,4 +24,14 @@ abstract class Resource extends Entity implements JsonSerializable, TableSeriali } abstract public function getType(): string; + + public function limitedJsonSerialize(): array { + return [ + 'id' => $this->id, + 'parentResource' => $this->parentResource, + 'organizationFolderId' => $this->organizationFolderId, + 'type' => $this->getType(), + 'name' => $this->name, + ]; + } } diff --git a/lib/Security/ResourceVoter.php b/lib/Security/ResourceVoter.php index c50a748..c35c5a9 100644 --- a/lib/Security/ResourceVoter.php +++ b/lib/Security/ResourceVoter.php @@ -40,7 +40,9 @@ class ResourceVoter extends Voter { /** @var Resource */ $resource = $subject; return match ($attribute) { - 'READ' => $this->isGranted($user, $resource), + 'READ' => $this->isGranted( $user, $resource), + // can read limited information about the resource (true: limited read is allowed, full read may be allowed, false: limited read is not allowed, full read may be allowed (!)) + 'READ_LIMITED' => $this->isGrantedLimitedRead($user, $resource), 'UPDATE' => $this->isGranted($user, $resource), 'DELETE' => $this->isGranted($user, $resource), 'UPDATE_MEMBERS' => $this->isGranted($user, $resource), @@ -108,6 +110,34 @@ class ResourceVoter extends Voter { || $this->isResourceManager($user, $resource, $resourceOrganizationFolder); } + protected function isGrantedLimitedRead(IUser $user, Resource $resource): bool { + $subResources = $this->resourceService->getAllSubResources($resource); + + foreach($subResources as $subResource) { + if($this->isManagerOfAnySubresource($user, $subResource)) { + return true; + } + } + + return false; + } + + protected function isManagerOfAnySubresource(IUser $user, Resource $resource) { + if($this->isGranted($user, $resource)) { + return true; + } + + $subResources = $this->resourceService->getAllSubResources($resource); + + foreach($subResources as $subResource) { + if($this->isManagerOfAnySubresource($user, $subResource)) { + return true; + } + } + + return false; + } + private function userIsInGroup(IUser $user, string $groupId): bool { return $this->groupManager->isInGroup($user->getUID(), $groupId); } diff --git a/src/views/ResourceSettings.vue b/src/views/ResourceSettings.vue index 03f0a0c..eb0c320 100644 --- a/src/views/ResourceSettings.vue +++ b/src/views/ResourceSettings.vue @@ -40,7 +40,7 @@ const saveInheritManagers = async (inheritManagers) => { watch(() => props.resourceId, async (newResourceId) => { loading.value = true; - resource.value = await api.getResource(newResourceId, "model+members"); + resource.value = await api.getResource(newResourceId, "model+members+subresources"); currentResourceName.value = resource.value.name; loading.value = false; }, { immediate: true });