diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 16d2c32..c45a009 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -17,6 +17,7 @@ use OCA\OrganizationFolders\Listener\SabrePluginAddListener; use OCA\OrganizationFolders\Listener\LoadAdditionalScripts; use OCA\OrganizationFolders\Security\AuthorizationService; use OCA\OrganizationFolders\Security\ResourceVoter; +use OCA\OrganizationFolders\Security\OrganizationFolderVoter; class Application extends App implements IBootstrap { public const APP_ID = 'organization_folders'; @@ -31,6 +32,7 @@ class Application extends App implements IBootstrap { $context->registerService(AuthorizationService::class, function (ContainerInterface $c) { $service = new AuthorizationService($c->get(IUserSession::class)); + $service->registerVoter($c->get(OrganizationFolderVoter::class)); $service->registerVoter($c->get(ResourceVoter::class)); return $service; }); diff --git a/lib/Errors/OrganizationNotFound.php b/lib/Errors/OrganizationNotFound.php index 48cdb68..9b29c41 100644 --- a/lib/Errors/OrganizationNotFound.php +++ b/lib/Errors/OrganizationNotFound.php @@ -3,7 +3,7 @@ namespace OCA\OrganizationFolders\Errors; class OrganizationNotFound extends NotFoundException { - public function __construct($provider, $id) { + public function __construct(string $provider, int $id) { parent::__construct(\OCA\OrganizationFolders\Model\Organization::class, ["provider" => $provider, "id" => $id]); } } \ No newline at end of file diff --git a/lib/Errors/OrganizationProviderNotFound.php b/lib/Errors/OrganizationProviderNotFound.php new file mode 100644 index 0000000..a8a99e6 --- /dev/null +++ b/lib/Errors/OrganizationProviderNotFound.php @@ -0,0 +1,9 @@ + $id]); + } +} \ No newline at end of file diff --git a/lib/OrganizationProvider/OrganizationProviderManager.php b/lib/OrganizationProvider/OrganizationProviderManager.php index 91cbb42..811ec7e 100644 --- a/lib/OrganizationProvider/OrganizationProviderManager.php +++ b/lib/OrganizationProvider/OrganizationProviderManager.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace OCA\OrganizationFolders\OrganizationProvider; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Server; +use OCA\OrganizationFolders\Errors\OrganizationProviderNotFound; use OCA\OrganizationFolders\Events\RegisterOrganizationProviderEvent; class OrganizationProviderManager { @@ -35,9 +35,15 @@ class OrganizationProviderManager { /** * @return OrganizationProvider + * @throws OrganizationProviderNotFound */ - public function getOrganizationProvider($id): ?OrganizationProvider { - return $this->organizationProviders[$id]; + public function getOrganizationProvider($id): OrganizationProvider { + $organizationProvider = $this->organizationProviders[$id]; + if(isset($organizationProvider)) { + return $organizationProvider; + } else { + throw new OrganizationProviderNotFound($id); + } } public function registerOrganizationProvider(OrganizationProvider $organizationProvider): self { diff --git a/lib/Security/OrganizationFolderVoter.php b/lib/Security/OrganizationFolderVoter.php new file mode 100644 index 0000000..8a7651a --- /dev/null +++ b/lib/Security/OrganizationFolderVoter.php @@ -0,0 +1,134 @@ + $this->isOrganizationFolderAdmin($user, $organizationFolder), + 'UPDATE' => $this->isOrganizationFolderAdmin($user, $organizationFolder), + 'DELETE' => $this->isOrganizationFolderAdmin($user, $organizationFolder), + 'UPDATE_MEMBERS' => $this->isOrganizationFolderAdmin($user, $organizationFolder), + 'MANAGE_ALL_RESOURCES' => $this->isOrganizationFolderAdmin($user, $organizationFolder), + + // At least Manager permissions required + 'READ_LIMITED' => $this->isOrganizationFolderAdminOrManager($user, $organizationFolder), + 'CREATE_RESOURCE' => $this->isOrganizationFolderAdminOrManager($user, $organizationFolder), + 'MANAGE_TOP_LEVEL_RESOURCES' => $this->isOrganizationFolderAdminOrManager($user, $organizationFolder), + + default => throw new \LogicException('This code should not be reached!') + }; + } + + /** + * @param IUser $user + * @param OrganizationFolder $organizationFolder + * @return bool + */ + private function isOrganizationFolderMember(IUser $user, OrganizationFolder $organizationFolder): bool { + $organizationFolderMembers = $this->organizationFolderMemberService->findAll($organizationFolder->getId()); + + foreach($organizationFolderMembers as $organizationFolderMember) { + if($this->userIsPrincipal($user, $organizationFolderMember->getPrincipal())) { + return true; + } + } + + return false; + } + + /** + * @param IUser $user + * @param OrganizationFolder $organizationFolder + * @return bool + */ + private function isOrganizationFolderAdmin(IUser $user, OrganizationFolder $organizationFolder): bool { + $organizationFolderMembers = $this->organizationFolderMemberService->findAll($organizationFolder->getId(), [ + "permissionLevel" => OrganizationFolderMemberPermissionLevel::ADMIN, + ]); + + foreach($organizationFolderMembers as $organizationFolderMember) { + // should be true for all returned members because of the filter, double check because of the big security implications + if($organizationFolderMember->getPermissionLevel() === OrganizationFolderMemberPermissionLevel::ADMIN->value) { + if($this->userIsPrincipal($user, $organizationFolderMember->getPrincipal())) { + return true; + } + } + } + + return false; + } + + /** + * @param IUser $user + * @param OrganizationFolder $organizationFolder + * @return bool + */ + private function isOrganizationFolderAdminOrManager(IUser $user, OrganizationFolder $organizationFolder): bool { + $organizationFolderMembers = $this->organizationFolderMemberService->findAll($organizationFolder->getId()); + + foreach($organizationFolderMembers as $organizationFolderMember) { + $permissionLevel = $organizationFolderMember->getPermissionLevel(); + if($permissionLevel === OrganizationFolderMemberPermissionLevel::ADMIN->value || $permissionLevel === OrganizationFolderMemberPermissionLevel::MANAGER->value) { + if($this->userIsPrincipal($user, $organizationFolderMember->getPrincipal())) { + return true; + } + } + } + + return false; + } + + private function userIsInGroup(IUser $user, string $groupId): bool { + return $this->groupManager->isInGroup($user->getUID(), $groupId); + } + + private function userHasRole(IUser $user, string $organizationProviderId, string $roleId): bool { + $organizationProvider = $this->organizationProviderManager->getOrganizationProvider($organizationProviderId); + $role = $organizationProvider->getRole($roleId); + + return $this->userIsInGroup($user, $role->getMembersGroup()); + } + + private function userIsPrincipal(IUser $user, Principal $principal): bool { + if($principal->getType() === PrincipalType::GROUP) { + return $this->userIsInGroup($user, $principal->getId()); + } else if($principal->getType() === PrincipalType::ROLE) { + [$organizationProviderId, $roleId] = explode(":", $principal->getId(), 2); + + return $this->userHasRole($user, $organizationProviderId, $roleId); + } else { + // user principals are not supported by Organization Folder Members and + // a principal object with that type should have never been put into this function + return false; + } + } +} diff --git a/lib/Security/ResourceVoter.php b/lib/Security/ResourceVoter.php index 7ca7935..c50a748 100644 --- a/lib/Security/ResourceVoter.php +++ b/lib/Security/ResourceVoter.php @@ -6,21 +6,25 @@ use OCP\IUser; use OCP\IGroupManager; use OCA\OrganizationFolders\Db\Resource; +use OCA\OrganizationFolders\Model\OrganizationFolder; +use OCA\OrganizationFolders\Service\OrganizationFolderService; use OCA\OrganizationFolders\Service\OrganizationFolderMemberService; use OCA\OrganizationFolders\Service\ResourceService; use OCA\OrganizationFolders\Service\ResourceMemberService; -use OCA\OrganizationFolders\Enum\OrganizationFolderMemberPermissionLevel; use OCA\OrganizationFolders\Enum\ResourceMemberPermissionLevel; use OCA\OrganizationFolders\Enum\PrincipalType; use OCA\OrganizationFolders\OrganizationProvider\OrganizationProviderManager; +use OCA\OrganizationFolders\Security\OrganizationFolderVoter; class ResourceVoter extends Voter { public function __construct( + private OrganizationFolderService $organizationFolderService, private OrganizationFolderMemberService $organizationFolderMemberService, private ResourceService $resourceService, private ResourceMemberService $resourceMemberService, private IGroupManager $groupManager, private OrganizationProviderManager $organizationProviderManager, + private OrganizationFolderVoter $organizationFolderVoter, ) { } protected function supports(string $attribute, mixed $subject): bool { @@ -29,8 +33,6 @@ class ResourceVoter extends Voter { protected function voteOnAttribute(string $attribute, mixed $subject, ?IUser $user): bool { - // _dlog($attribute, $subject); - if (!$user) { return false; } @@ -47,31 +49,8 @@ class ResourceVoter extends Voter { }; } - private function isResourceOrganizationFolderAdmin(IUser $user, Resource $resource): bool { - $organizationFolderMembers = $this->organizationFolderMemberService->findAll($resource->getOrganizationFolderId(), [ - "permissionLevel" => OrganizationFolderMemberPermissionLevel::ADMIN, - ]); - - foreach($organizationFolderMembers as $organizationFolderMember) { - // should be true for all returned members because of the filter, double check because of the big security implications - if($organizationFolderMember->getPermissionLevel() === OrganizationFolderMemberPermissionLevel::ADMIN->value) { - $principal = $organizationFolderMember->getPrincipal(); - - if($principal->getType() === PrincipalType::GROUP) { - if($this->userIsInGroup($user, $principal->getId())) { - return true; - } - } else if($principal->getType() === PrincipalType::ROLE) { - [$organizationProviderId, $roleId] = explode(":", $principal->getId(), 2); - - if($this->userHasRole($user, $organizationProviderId, $roleId)) { - return true; - } - } - } - } - - return false; + private function allowedToManageAllResourcesInOrganizationFolder(IUser $user, OrganizationFolder $resourceOrganizationFolder): bool { + return $this->organizationFolderVoter->vote($user, $resourceOrganizationFolder, ["MANAGE_ALL_RESOURCES"]) === self::ACCESS_GRANTED; } /** @@ -79,9 +58,7 @@ class ResourceVoter extends Voter { * @param Resource $resource * @return bool */ - private function isResourceManager(IUser $user, Resource $resource): bool { - // TODO: check if is top-level resource and user is organizationFolder manager - + private function isResourceManager(IUser $user, Resource $resource, OrganizationFolder $resourceOrganizationFolder): bool { $resourceMembers = $this->resourceMemberService->findAll($resource->getId()); foreach($resourceMembers as $resourceMember) { @@ -106,11 +83,18 @@ class ResourceVoter extends Voter { } } + // inherit manager permissions from level above if($resource->getInheritManagers()) { - $parentResource = $this->resourceService->getParentResource($resource); + if(!is_null($resource->getParentResource())) { + // not top-level resource -> allowed to manage resource if allowed to manage parent resource + $parentResource = $this->resourceService->getParentResource($resource); - if(!is_null($parentResource)) { - return $this->isResourceManager($user, $parentResource); + if(!is_null($parentResource)) { + return $this->isResourceManager($user, $parentResource, $resourceOrganizationFolder); + } + } else { + // top-level resource -> allowed to manage resource if manager of organization folder + return $this->organizationFolderVoter->vote($user, $resourceOrganizationFolder, ["MANAGE_TOP_LEVEL_RESOURCES"]) === self::ACCESS_GRANTED; } } @@ -118,7 +102,10 @@ class ResourceVoter extends Voter { } protected function isGranted(IUser $user, Resource $resource): bool { - return $this->isResourceOrganizationFolderAdmin($user, $resource) || $this->isResourceManager($user, $resource); + $resourceOrganizationFolder = $this->organizationFolderService->find($resource->getOrganizationFolderId()); + + return $this->allowedToManageAllResourcesInOrganizationFolder($user, $resourceOrganizationFolder) + || $this->isResourceManager($user, $resource, $resourceOrganizationFolder); } private function userIsInGroup(IUser $user, string $groupId): bool {