diff --git a/config/services/providers.yml b/config/services/providers.yml index cb784988..226c4e81 100644 --- a/config/services/providers.yml +++ b/config/services/providers.yml @@ -2,7 +2,3 @@ services: PhpList\Core\Domain\Subscription\Service\Provider\SubscriberProvider: autowire: true autoconfigure: true - - PhpList\Core\Domain\Messaging\Service\Provider\BounceActionProvider: - autowire: true - autoconfigure: true diff --git a/config/services/services.yml b/config/services/services.yml index 1f509787..f1b68e74 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -108,9 +108,7 @@ services: arguments: - !tagged_iterator { tag: 'phplist.bounce_action_handler' } - # I18n - PhpList\Core\Domain\Common\I18n\SimpleTranslator: + PhpList\Core\Domain\Identity\Service\PermissionChecker: autowire: true autoconfigure: true - - PhpList\Core\Domain\Common\I18n\TranslatorInterface: '@PhpList\Core\Domain\Common\I18n\SimpleTranslator' + public: true diff --git a/src/Domain/Common/Model/Interfaces/OwnableInterface.php b/src/Domain/Common/Model/Interfaces/OwnableInterface.php new file mode 100644 index 00000000..16e54e40 --- /dev/null +++ b/src/Domain/Common/Model/Interfaces/OwnableInterface.php @@ -0,0 +1,12 @@ +modifiedBy; } + + public function owns(OwnableInterface $resource): bool + { + if ($this->getId() === null) { + return false; + } + + return $resource->getOwner()->getId() === $this->getId(); + } } diff --git a/src/Domain/Identity/Service/PermissionChecker.php b/src/Domain/Identity/Service/PermissionChecker.php new file mode 100644 index 00000000..8fc241b7 --- /dev/null +++ b/src/Domain/Identity/Service/PermissionChecker.php @@ -0,0 +1,89 @@ + PrivilegeFlag::Subscribers, + SubscriberList::class => PrivilegeFlag::Subscribers, + Message::class => PrivilegeFlag::Campaigns, + ]; + + private const OWNERSHIP_MAP = [ + Subscriber::class => SubscriberList::class, + Message::class => SubscriberList::class + ]; + + public function canManage(Administrator $actor, DomainModel $resource): bool + { + if ($actor->isSuperUser()) { + return true; + } + + $required = $this->resolveRequiredPrivilege($resource); + if ($required !== null && !$actor->getPrivileges()->has($required)) { + return false; + } + + if ($resource instanceof OwnableInterface) { + return $actor->owns($resource); + } + + $notRestricted = true; + foreach (self::OWNERSHIP_MAP as $resourceClass => $relatedClass) { + if ($resource instanceof $resourceClass) { + $related = $this->resolveRelatedEntity($resource, $relatedClass); + $notRestricted = $this->checkRelatedResources($related, $actor); + } + } + + return $notRestricted; + } + + private function resolveRequiredPrivilege(DomainModel $resource): ?PrivilegeFlag + { + foreach (self::REQUIRED_PRIVILEGE_MAP as $class => $flag) { + if ($resource instanceof $class) { + return $flag; + } + } + + return null; + } + + /** @return OwnableInterface[] */ + private function resolveRelatedEntity(DomainModel $resource, string $relatedClass): array + { + if ($resource instanceof Subscriber && $relatedClass === SubscriberList::class) { + return $resource->getSubscribedLists()->toArray(); + } + + if ($resource instanceof Message && $relatedClass === SubscriberList::class) { + return $resource->getListMessages()->map(fn($lm) => $lm->getSubscriberList())->toArray(); + } + + return []; + } + + private function checkRelatedResources(array $related, Administrator $actor): bool + { + foreach ($related as $relatedResource) { + if ($actor->owns($relatedResource)) { + return true; + } + } + + return false; + } +} diff --git a/src/Domain/Messaging/Model/Message.php b/src/Domain/Messaging/Model/Message.php index fbbfec8a..5064c4f1 100644 --- a/src/Domain/Messaging/Model/Message.php +++ b/src/Domain/Messaging/Model/Message.php @@ -11,6 +11,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Common\Model\Interfaces\Identity; use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate; +use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; @@ -23,7 +24,7 @@ #[ORM\Table(name: 'phplist_message')] #[ORM\Index(name: 'uuididx', columns: ['uuid'])] #[ORM\HasLifecycleCallbacks] -class Message implements DomainModel, Identity, ModificationDate +class Message implements DomainModel, Identity, ModificationDate, OwnableInterface { #[ORM\Id] #[ORM\Column(type: 'integer')] diff --git a/src/Domain/Subscription/Model/SubscribePage.php b/src/Domain/Subscription/Model/SubscribePage.php index e4696380..979b3c4c 100644 --- a/src/Domain/Subscription/Model/SubscribePage.php +++ b/src/Domain/Subscription/Model/SubscribePage.php @@ -7,12 +7,13 @@ use Doctrine\ORM\Mapping as ORM; use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Common\Model\Interfaces\Identity; +use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Subscription\Repository\SubscriberPageRepository; #[ORM\Entity(repositoryClass: SubscriberPageRepository::class)] #[ORM\Table(name: 'phplist_subscribepage')] -class SubscribePage implements DomainModel, Identity +class SubscribePage implements DomainModel, Identity, OwnableInterface { #[ORM\Id] #[ORM\Column(type: 'integer')] diff --git a/src/Domain/Subscription/Model/SubscriberList.php b/src/Domain/Subscription/Model/SubscriberList.php index 947cbe26..32f85f5d 100644 --- a/src/Domain/Subscription/Model/SubscriberList.php +++ b/src/Domain/Subscription/Model/SubscriberList.php @@ -12,6 +12,7 @@ use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Common\Model\Interfaces\Identity; use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate; +use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Messaging\Model\ListMessage; use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; @@ -28,7 +29,7 @@ #[ORM\Index(name: 'nameidx', columns: ['name'])] #[ORM\Index(name: 'listorderidx', columns: ['listorder'])] #[ORM\HasLifecycleCallbacks] -class SubscriberList implements DomainModel, Identity, CreationDate, ModificationDate +class SubscriberList implements DomainModel, Identity, CreationDate, ModificationDate, OwnableInterface { #[ORM\Id] #[ORM\Column(type: 'integer')] diff --git a/tests/Integration/Domain/Identity/Service/PermissionCheckerTest.php b/tests/Integration/Domain/Identity/Service/PermissionCheckerTest.php new file mode 100644 index 00000000..50820026 --- /dev/null +++ b/tests/Integration/Domain/Identity/Service/PermissionCheckerTest.php @@ -0,0 +1,35 @@ +checker = self::getContainer()->get(PermissionChecker::class); + } + + public function testServiceIsRegisteredInContainer(): void + { + self::assertInstanceOf(PermissionChecker::class, $this->checker); + self::assertSame($this->checker, self::getContainer()->get(PermissionChecker::class)); + } + + public function testSuperUserCanManageAnyResource(): void + { + $admin = new Administrator(); + $admin->setSuperUser(true); + $resource = $this->createMock(SubscriberList::class); + $this->assertTrue($this->checker->canManage($admin, $resource)); + } +}