0
0
Fork 0
mirror of https://github.com/verdigado/organization_folders.git synced 2024-12-06 11:22:41 +01:00

Added security classes and draft version of ResourceVoter

This commit is contained in:
Jonathan Treffler 2024-11-06 22:11:16 +01:00
parent 22c06b5689
commit 88cb258c2b
11 changed files with 428 additions and 0 deletions

View file

@ -0,0 +1,34 @@
<?php
namespace OCA\OrganizationFolders\Security;
class AffirmativeStrategy implements \Stringable {
private bool $allowIfAllAbstainDecisions;
public function __construct(bool $allowIfAllAbstainDecisions = false) {
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions;
}
public function decide(\Traversable $results): bool {
$deny = 0;
foreach ($results as $result) {
if (VoterInterface::ACCESS_GRANTED === $result) {
return true;
}
if (VoterInterface::ACCESS_DENIED === $result) {
++$deny;
}
}
if ($deny > 0) {
return false;
}
return $this->allowIfAllAbstainDecisions;
}
public function __toString(): string {
return 'affirmative';
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace OCA\OrganizationFolders\Security;
use OCP\IUserSession;
class AuthorizationService {
private const VALID_VOTES = [
VoterInterface::ACCESS_GRANTED => true,
VoterInterface::ACCESS_DENIED => true,
VoterInterface::ACCESS_ABSTAIN => true,
];
/**
* @var Voter[]
*/
private array $voters = [];
private $strategy;
public function __construct(private IUserSession $userSession) {
$this->strategy = new AffirmativeStrategy();
}
public function registerVoter(Voter $voter): self {
$this->voters[] = $voter;
return $this;
}
public function isGranted(array $attributes, $subject) {
return $this->strategy->decide(
$this->collectResults($attributes, $subject)
);
}
private function collectResults(array $attributes, $subject): \Traversable {
$user = $this->userSession->getUser();
foreach ($this->voters as $voter) {
$result = $voter->vote($user, $subject, $attributes);
if (!\is_int($result) || !(self::VALID_VOTES[$result] ?? false)) {
throw new \LogicException(sprintf('"%s::vote()" must return one of "%s" constants ("ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN"), "%s" returned.', get_debug_type($voter), VoterInterface::class, var_export($result, true)));
}
yield $result;
}
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace OCA\OrganizationFolders\Security;
use OCP\IUser;
use OCP\IGroupManager;
use OCA\OrganizationFolders\Db\Resource;
use OCA\OrganizationFolders\Service\ResourceService;
use OCA\OrganizationFolders\Service\ResourceMemberService;
use OCA\OrganizationFolders\Enum\MemberPermissionLevel;
use OCA\OrganizationFolders\Enum\MemberType;
use OCA\OrganizationFolders\OrganizationProvider\OrganizationProviderManager;
class ResourceVoter extends Voter {
public function __construct(
private ResourceService $resourceService,
private ResourceMemberService $resourceMemberService,
private IGroupManager $groupManager,
private OrganizationProviderManager $organizationProviderManager,
) {
}
protected function supports(string $attribute, mixed $subject): bool {
return $subject instanceof Resource || $subject === Resource::class;
}
protected function voteOnAttribute(string $attribute, mixed $subject, ?IUser $user): bool {
// _dlog($attribute, $subject);
if (!$user) {
return false;
}
/** @var Resource */
$resource = $subject;
return match ($attribute) {
'READ' => $this->isGranted($user, $resource),
'UPDATE' => $this->isGranted($user, $resource),
'DELETE' => $this->isGranted($user, $resource),
default => throw new \LogicException('This code should not be reached!')
};
}
private function isResourceOrganizationFolderAdmin(IUser $user, Resource $resource): bool {
// TODO: implement
return false;
}
/**
* @param IUser $user
* @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
$resourceMembers = $this->resourceMemberService->findAll($resource->getId());
foreach($resourceMembers as $resourceMember) {
if($resourceMember->getPermissionLevel() === MemberPermissionLevel::MANAGER->value) {
if($resourceMember->getType() === MemberType::USER->value) {
if($resourceMember->getPrincipal() === $user->getUID()) {
return true;
}
} else if($resourceMember->getType() === MemberType::GROUP->value) {
if($this->groupManager->isInGroup($user->getUID(), $resourceMember->getPrincipal())) {
return true;
}
} else if($resourceMember->getType() === MemberType::ROLE->value) {
['organizationProviderId' => $organizationProviderId, 'roleId' => $roleId] = $resourceMember->getParsedPrincipal();
$organizationProvider = $this->organizationProviderManager->getOrganizationProvider($organizationProviderId);
$role = $organizationProvider->getRole($roleId);
if($this->groupManager->isInGroup($user->getUID(), $role->getMembersGroup())) {
return true;
}
}
}
}
if($resource->getInheritManagers()) {
$parentResource = $this->resourceService->getParentResource($resource);
if(!is_null($parentResource)) {
return $this->isResourceManager($user, $resource);
}
}
return false;
}
protected function isGranted(IUser $user, Resource $resource): bool {
return $this->isResourceOrganizationFolderAdmin($user, $resource) || $this->isResourceManager($user, $resource);
}
}

67
lib/Security/Voter.php Normal file
View file

@ -0,0 +1,67 @@
<?php
namespace OCA\OrganizationFolders\Security;
use OCP\IUser;
abstract class Voter implements VoterInterface {
public function vote(?IUser $user, mixed $subject, array $attributes): int {
// abstain vote by default in case none of the attributes are supported
$vote = self::ACCESS_ABSTAIN;
foreach ($attributes as $attribute) {
try {
if (!$this->supports($attribute, $subject)) {
continue;
}
} catch (\TypeError $e) {
if (str_contains($e->getMessage(), 'supports(): Argument #1')) {
continue;
}
throw $e;
}
// as soon as at least one attribute is supported, default is to deny access
$vote = self::ACCESS_DENIED;
if ($this->voteOnAttribute($attribute, $subject, $user)) {
// grant access as soon as at least one attribute returns a positive response
return self::ACCESS_GRANTED;
}
}
return $vote;
}
/**
* Return false if your voter doesn't support the given attribute. Symfony will cache
* that decision and won't call your voter again for that attribute.
*/
public function supportsAttribute(string $attribute): bool {
return true;
}
/**
* Return false if your voter doesn't support the given subject type. Symfony will cache
* that decision and won't call your voter again for that subject type.
*
* @param string $subjectType The type of the subject inferred by `get_class()` or `get_debug_type()`
*/
public function supportsType(string $subjectType): bool {
return true;
}
/**
* Determines if the attribute and subject are supported by this voter.
*
* @param $subject The subject to secure, e.g. an object the user wants to access or any other PHP type
*/
abstract protected function supports(string $attribute, mixed $subject): bool;
/**
* Perform a single access check operation on a given attribute, subject and token.
* It is safe to assume that $attribute and $subject already passed the "supports()" method check.
*/
abstract protected function voteOnAttribute(string $attribute, mixed $subject, ?IUser $user): bool;
}

View file

@ -0,0 +1,24 @@
<?php
namespace OCA\OrganizationFolders\Security;
use OCP\IUser;
interface VoterInterface {
public const ACCESS_GRANTED = 1;
public const ACCESS_ABSTAIN = 0;
public const ACCESS_DENIED = -1;
/**
* Returns the vote for the given parameters.
*
* This method must return one of the following constants:
* ACCESS_GRANTED, ACCESS_DENIED, or ACCESS_ABSTAIN.
*
* @param mixed $subject The subject to secure
* @param array $attributes An array of attributes associated with the method being invoked
*
* @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED
*/
public function vote(?IUser $user, mixed $subject, array $attributes): int;
}