Skip to content

Commit 936cabc

Browse files
TatevikGrtatevikg1
andauthored
Access level check (#358)
* OwnableInterface * PermissionChecker * Check related * Register service + test * Style fix --------- Co-authored-by: Tatevik <tatevikg1@gmail.com>
1 parent 793c260 commit 936cabc

File tree

9 files changed

+154
-11
lines changed

9 files changed

+154
-11
lines changed

config/services/providers.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,3 @@ services:
22
PhpList\Core\Domain\Subscription\Service\Provider\SubscriberProvider:
33
autowire: true
44
autoconfigure: true
5-
6-
PhpList\Core\Domain\Messaging\Service\Provider\BounceActionProvider:
7-
autowire: true
8-
autoconfigure: true

config/services/services.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ services:
108108
arguments:
109109
- !tagged_iterator { tag: 'phplist.bounce_action_handler' }
110110

111-
# I18n
112-
PhpList\Core\Domain\Common\I18n\SimpleTranslator:
111+
PhpList\Core\Domain\Identity\Service\PermissionChecker:
113112
autowire: true
114113
autoconfigure: true
115-
116-
PhpList\Core\Domain\Common\I18n\TranslatorInterface: '@PhpList\Core\Domain\Common\I18n\SimpleTranslator'
114+
public: true
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Common\Model\Interfaces;
6+
7+
use PhpList\Core\Domain\Identity\Model\Administrator;
8+
9+
interface OwnableInterface
10+
{
11+
public function getOwner(): ?Administrator;
12+
}

src/Domain/Identity/Model/Administrator.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
1212
use PhpList\Core\Domain\Common\Model\Interfaces\Identity;
1313
use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate;
14+
use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface;
1415
use PhpList\Core\Domain\Identity\Repository\AdministratorRepository;
1516

1617
/**
@@ -221,4 +222,13 @@ public function getModifiedBy(): ?string
221222
{
222223
return $this->modifiedBy;
223224
}
225+
226+
public function owns(OwnableInterface $resource): bool
227+
{
228+
if ($this->getId() === null) {
229+
return false;
230+
}
231+
232+
return $resource->getOwner()->getId() === $this->getId();
233+
}
224234
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Identity\Service;
6+
7+
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
8+
use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface;
9+
use PhpList\Core\Domain\Identity\Model\Administrator;
10+
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
11+
use PhpList\Core\Domain\Messaging\Model\Message;
12+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
13+
use PhpList\Core\Domain\Subscription\Model\SubscriberList;
14+
15+
class PermissionChecker
16+
{
17+
private const REQUIRED_PRIVILEGE_MAP = [
18+
Subscriber::class => PrivilegeFlag::Subscribers,
19+
SubscriberList::class => PrivilegeFlag::Subscribers,
20+
Message::class => PrivilegeFlag::Campaigns,
21+
];
22+
23+
private const OWNERSHIP_MAP = [
24+
Subscriber::class => SubscriberList::class,
25+
Message::class => SubscriberList::class
26+
];
27+
28+
public function canManage(Administrator $actor, DomainModel $resource): bool
29+
{
30+
if ($actor->isSuperUser()) {
31+
return true;
32+
}
33+
34+
$required = $this->resolveRequiredPrivilege($resource);
35+
if ($required !== null && !$actor->getPrivileges()->has($required)) {
36+
return false;
37+
}
38+
39+
if ($resource instanceof OwnableInterface) {
40+
return $actor->owns($resource);
41+
}
42+
43+
$notRestricted = true;
44+
foreach (self::OWNERSHIP_MAP as $resourceClass => $relatedClass) {
45+
if ($resource instanceof $resourceClass) {
46+
$related = $this->resolveRelatedEntity($resource, $relatedClass);
47+
$notRestricted = $this->checkRelatedResources($related, $actor);
48+
}
49+
}
50+
51+
return $notRestricted;
52+
}
53+
54+
private function resolveRequiredPrivilege(DomainModel $resource): ?PrivilegeFlag
55+
{
56+
foreach (self::REQUIRED_PRIVILEGE_MAP as $class => $flag) {
57+
if ($resource instanceof $class) {
58+
return $flag;
59+
}
60+
}
61+
62+
return null;
63+
}
64+
65+
/** @return OwnableInterface[] */
66+
private function resolveRelatedEntity(DomainModel $resource, string $relatedClass): array
67+
{
68+
if ($resource instanceof Subscriber && $relatedClass === SubscriberList::class) {
69+
return $resource->getSubscribedLists()->toArray();
70+
}
71+
72+
if ($resource instanceof Message && $relatedClass === SubscriberList::class) {
73+
return $resource->getListMessages()->map(fn($lm) => $lm->getSubscriberList())->toArray();
74+
}
75+
76+
return [];
77+
}
78+
79+
private function checkRelatedResources(array $related, Administrator $actor): bool
80+
{
81+
foreach ($related as $relatedResource) {
82+
if ($actor->owns($relatedResource)) {
83+
return true;
84+
}
85+
}
86+
87+
return false;
88+
}
89+
}

src/Domain/Messaging/Model/Message.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
1212
use PhpList\Core\Domain\Common\Model\Interfaces\Identity;
1313
use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate;
14+
use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface;
1415
use PhpList\Core\Domain\Identity\Model\Administrator;
1516
use PhpList\Core\Domain\Messaging\Model\Message\MessageContent;
1617
use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat;
@@ -23,7 +24,7 @@
2324
#[ORM\Table(name: 'phplist_message')]
2425
#[ORM\Index(name: 'uuididx', columns: ['uuid'])]
2526
#[ORM\HasLifecycleCallbacks]
26-
class Message implements DomainModel, Identity, ModificationDate
27+
class Message implements DomainModel, Identity, ModificationDate, OwnableInterface
2728
{
2829
#[ORM\Id]
2930
#[ORM\Column(type: 'integer')]

src/Domain/Subscription/Model/SubscribePage.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
use Doctrine\ORM\Mapping as ORM;
88
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
99
use PhpList\Core\Domain\Common\Model\Interfaces\Identity;
10+
use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface;
1011
use PhpList\Core\Domain\Identity\Model\Administrator;
1112
use PhpList\Core\Domain\Subscription\Repository\SubscriberPageRepository;
1213

1314
#[ORM\Entity(repositoryClass: SubscriberPageRepository::class)]
1415
#[ORM\Table(name: 'phplist_subscribepage')]
15-
class SubscribePage implements DomainModel, Identity
16+
class SubscribePage implements DomainModel, Identity, OwnableInterface
1617
{
1718
#[ORM\Id]
1819
#[ORM\Column(type: 'integer')]

src/Domain/Subscription/Model/SubscriberList.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
1313
use PhpList\Core\Domain\Common\Model\Interfaces\Identity;
1414
use PhpList\Core\Domain\Common\Model\Interfaces\ModificationDate;
15+
use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface;
1516
use PhpList\Core\Domain\Identity\Model\Administrator;
1617
use PhpList\Core\Domain\Messaging\Model\ListMessage;
1718
use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository;
@@ -28,7 +29,7 @@
2829
#[ORM\Index(name: 'nameidx', columns: ['name'])]
2930
#[ORM\Index(name: 'listorderidx', columns: ['listorder'])]
3031
#[ORM\HasLifecycleCallbacks]
31-
class SubscriberList implements DomainModel, Identity, CreationDate, ModificationDate
32+
class SubscriberList implements DomainModel, Identity, CreationDate, ModificationDate, OwnableInterface
3233
{
3334
#[ORM\Id]
3435
#[ORM\Column(type: 'integer')]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Integration\Domain\Identity\Service;
6+
7+
use PhpList\Core\Domain\Identity\Model\Administrator;
8+
use PhpList\Core\Domain\Identity\Service\PermissionChecker;
9+
use PhpList\Core\Domain\Subscription\Model\SubscriberList;
10+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
11+
12+
final class PermissionCheckerTest extends KernelTestCase
13+
{
14+
private PermissionChecker $checker;
15+
16+
protected function setUp(): void
17+
{
18+
parent::setUp();
19+
$this->checker = self::getContainer()->get(PermissionChecker::class);
20+
}
21+
22+
public function testServiceIsRegisteredInContainer(): void
23+
{
24+
self::assertInstanceOf(PermissionChecker::class, $this->checker);
25+
self::assertSame($this->checker, self::getContainer()->get(PermissionChecker::class));
26+
}
27+
28+
public function testSuperUserCanManageAnyResource(): void
29+
{
30+
$admin = new Administrator();
31+
$admin->setSuperUser(true);
32+
$resource = $this->createMock(SubscriberList::class);
33+
$this->assertTrue($this->checker->canManage($admin, $resource));
34+
}
35+
}

0 commit comments

Comments
 (0)