Skip to content

Commit 81113bc

Browse files
committed
feat(linksecurity): expand functionality to cover all combinations of to and from property and add optional object name
1 parent 26e7057 commit 81113bc

File tree

8 files changed

+120
-9
lines changed

8 files changed

+120
-9
lines changed

features/authorization/deny.feature

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,23 +215,50 @@ Feature: Authorization checking
215215
When I add "Accept" header equal to "application/ld+json"
216216
And I add "Content-Type" header equal to "application/ld+json"
217217
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
218-
And I send a "GET" request to "/secured_dummies/40000/related"
218+
And I send a "GET" request to "/secured_dummies/40000/to_from"
219219
Then the response status code should be 404
220220

221221
Scenario: An user can get related linked dummies for an secured dummy they own
222222
Given there are 1 SecuredDummy objects owned by dunglas with related dummies
223223
When I add "Accept" header equal to "application/ld+json"
224224
And I add "Content-Type" header equal to "application/ld+json"
225225
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
226-
And I send a "GET" request to "/secured_dummies/4/related"
226+
And I send a "GET" request to "/secured_dummies/4/to_from"
227227
Then the response status code should be 200
228228
And the response should contain "securedDummy"
229229
And the JSON node "hydra:member[0].id" should be equal to 1"
230230
231+
Scenario: I define a custom name of the security object
232+
When I add "Accept" header equal to "application/ld+json"
233+
And I add "Content-Type" header equal to "application/ld+json"
234+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
235+
And I send a "GET" request to "/secured_dummies/4/with_name"
236+
Then the response status code should be 200
237+
And the response should contain "securedDummy"
238+
And the JSON node "hydra:member[0].id" should be equal to 1"
239+
240+
Scenario: I define a from from link
241+
When I add "Accept" header equal to "application/ld+json"
242+
And I add "Content-Type" header equal to "application/ld+json"
243+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
244+
And I send a "GET" request to "/related_linked_dummies/1/from_from"
245+
Then the response status code should be 200
246+
And the response should contain "id"
247+
And the JSON node "hydra:member[0].id" should be equal to 4"
248+
249+
Scenario: I define multiple links with security
250+
When I add "Accept" header equal to "application/ld+json"
251+
And I add "Content-Type" header equal to "application/ld+json"
252+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
253+
And I send a "GET" request to "/secured_dummies/4/related/1"
254+
Then the response status code should be 200
255+
And the response should contain "id"
256+
And the JSON node "hydra:member[0].id" should be equal to 1"
257+
231258
Scenario: An user can not get related linked dummies for an secured dummy they do not own
232259
Given there are 1 SecuredDummy objects owned by someone with related dummies
233260
When I add "Accept" header equal to "application/ld+json"
234261
And I add "Content-Type" header equal to "application/ld+json"
235262
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
236-
And I send a "GET" request to "/secured_dummies/5/related"
263+
And I send a "GET" request to "/secured_dummies/5/to_from"
237264
Then the response status code should be 403

src/Metadata/Link.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
1717
final class Link
1818
{
19-
public function __construct(private ?string $parameterName = null, private ?string $fromProperty = null, private ?string $toProperty = null, private ?string $fromClass = null, private ?string $toClass = null, private ?array $identifiers = null, private ?bool $compositeIdentifier = null, private ?string $expandedValue = null, private ?string $security = null, private ?string $securityMessage = null)
19+
public function __construct(private ?string $parameterName = null, private ?string $fromProperty = null, private ?string $toProperty = null, private ?string $fromClass = null, private ?string $toClass = null, private ?array $identifiers = null, private ?bool $compositeIdentifier = null, private ?string $expandedValue = null, private ?string $security = null, private ?string $securityMessage = null, private ?string $securityObjectName = null)
2020
{
2121
// For the inverse property shortcut
2222
if ($this->parameterName && class_exists($this->parameterName)) {
@@ -154,6 +154,19 @@ public function withSecurityMessage(?string $securityMessage): self
154154
return $self;
155155
}
156156

157+
public function getSecurityObjectName(): ?string
158+
{
159+
return $this->securityObjectName;
160+
}
161+
162+
public function withSecurityObjectName(?string $securityObjectName): self
163+
{
164+
$self = clone $this;
165+
$self->securityObjectName = $securityObjectName;
166+
167+
return $self;
168+
}
169+
157170
public function withLink(self $link): self
158171
{
159172
$self = clone $this;
@@ -198,6 +211,10 @@ public function withLink(self $link): self
198211
$self->securityMessage = $securityMessage;
199212
}
200213

214+
if (!$self->getSecurityObjectName() && ($securityObjectName = $link->getSecurityObjectName())) {
215+
$self->securityObjectName = $securityObjectName;
216+
}
217+
201218
return $self;
202219
}
203220
}

src/Symfony/EventListener/DenyAccessListener.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,13 @@ private function checkSecurity(Request $request, string $attribute, array $extra
101101
continue;
102102
}
103103

104-
if (!$this->resourceAccessChecker->isGranted($uriVariable->getFromClass(), $uriVariable->getSecurity(), $extraVariables)) {
104+
$targetResource = $uriVariable->getFromClass() ?? $uriVariable->getToClass();
105+
106+
if (!$targetResource) {
107+
continue;
108+
}
109+
110+
if (!$this->resourceAccessChecker->isGranted($targetResource, $uriVariable->getSecurity(), $extraVariables)) {
105111
throw new AccessDeniedException($uriVariable->getSecurityMessage() ?? 'Access Denied.');
106112
}
107113
}

src/Symfony/EventListener/ReadListener.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,28 @@ public function onKernelRequest(RequestEvent $event): void
110110
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
111111
continue;
112112
}
113-
$relationClass = $uriVariable->getFromClass();
113+
$relationClass = $uriVariable->getFromClass() ?? $uriVariable->getToClass();
114+
115+
if (!$relationClass) {
116+
continue;
117+
}
118+
114119
try {
115120
$tmp = $this->provider->provide($this->resourceMetadataCollectionFactory->create($relationClass)->getOperation(null, false, true), [$uriVariable->getIdentifiers()[0] => $parameters[$key]], $context);
116121
if (null === $tmp) {
117122
throw new NotFoundHttpException('Not Found');
118123
}
119-
$request->attributes->set($uriVariable->getToProperty(), $tmp);
124+
$securityObjectName = $uriVariable->getSecurityObjectName();
125+
126+
if (!$securityObjectName) {
127+
$securityObjectName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
128+
}
129+
130+
if (!$securityObjectName) {
131+
continue;
132+
}
133+
134+
$request->attributes->set($securityObjectName, $tmp);
120135
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
121136
throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
122137
}

tests/Fixtures/TestBundle/Document/RelatedLinkedDummy.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,27 @@
2121

2222
#[ApiResource()]
2323
#[ApiResource(
24-
uriTemplate: '/secured_dummies/{securedDummyId}/related',
24+
uriTemplate: '/secured_dummies/{securedDummyId}/to_from',
2525
operations: [new GetCollection()],
2626
uriVariables: [
2727
'securedDummyId' => new Link(toProperty: 'securedDummy', fromClass: SecuredDummy::class, security: "is_granted('ROLE_USER') and securedDummy.getOwner() == user"),
2828
]
2929
)]
30+
#[ApiResource(
31+
uriTemplate: '/secured_dummies/{securedDummyId}/with_name',
32+
operations: [new GetCollection()],
33+
uriVariables: [
34+
'securedDummyId' => new Link(toProperty: 'securedDummy', fromClass: SecuredDummy::class, security: "is_granted('ROLE_USER') and testObj.getOwner() == user", securityObjectName: 'testObj'),
35+
]
36+
)]
37+
#[ApiResource(
38+
uriTemplate: '/secured_dummies/{securedDummyId}/related/{id}',
39+
operations: [new GetCollection()],
40+
uriVariables: [
41+
'securedDummyId' => new Link(toProperty: 'securedDummy', fromClass: SecuredDummy::class, security: "is_granted('ROLE_USER') and securedDummy.getOwner() == user"),
42+
'id' => new Link(fromClass: RelatedLinkedDummy::class, security: "is_granted('ROLE_USER') and testObj.getSecuredDummy().getOwner() == user", securityObjectName: 'testObj'),
43+
]
44+
)]
3045
#[ODM\Document]
3146
class RelatedLinkedDummy
3247
{

tests/Fixtures/TestBundle/Document/SecuredDummy.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Metadata\GraphQl\Mutation;
2121
use ApiPlatform\Metadata\GraphQl\Query;
2222
use ApiPlatform\Metadata\GraphQl\QueryCollection;
23+
use ApiPlatform\Metadata\Link;
2324
use ApiPlatform\Metadata\Post;
2425
use ApiPlatform\Metadata\Put;
2526
use Doctrine\Common\Collections\ArrayCollection;
@@ -34,6 +35,13 @@
3435
* @author Alan Poulain <[email protected]>
3536
*/
3637
#[ApiResource(operations: [new Get(security: 'is_granted(\'ROLE_USER\') and object.getOwner() == user'), new Put(securityPostDenormalize: 'is_granted(\'ROLE_USER\') and previous_object.getOwner() == user'), new GetCollection(security: 'is_granted(\'ROLE_USER\') or is_granted(\'ROLE_ADMIN\')'), new GetCollection(uriTemplate: 'custom_data_provider_generator', security: 'is_granted(\'ROLE_USER\')'), new Post(security: 'is_granted(\'ROLE_ADMIN\')')], graphQlOperations: [new Query(name: 'item_query', security: 'is_granted(\'ROLE_ADMIN\') or (is_granted(\'ROLE_USER\') and object.getOwner() == user)'), new QueryCollection(name: 'collection_query', security: 'is_granted(\'ROLE_ADMIN\')'), new Mutation(name: 'delete'), new Mutation(name: 'update', securityPostDenormalize: 'is_granted(\'ROLE_USER\') and previous_object.getOwner() == user'), new Mutation(name: 'create', security: 'is_granted(\'ROLE_ADMIN\')', securityMessage: 'Only admins can create a secured dummy.')], security: 'is_granted(\'ROLE_USER\')')]
38+
#[ApiResource(
39+
uriTemplate: '/related_linked_dummies/{relatedDummyId}/from_from',
40+
operations: [new GetCollection()],
41+
uriVariables: [
42+
'relatedDummyId' => new Link(fromProperty: 'securedDummy', fromClass: RelatedLinkedDummy::class, security: "is_granted('ROLE_USER') and relatedDummy.getSecuredDummy().getOwner() == user", securityObjectName: 'relatedDummy'),
43+
]
44+
)]
3745
#[ODM\Document]
3846
class SecuredDummy
3947
{

tests/Fixtures/TestBundle/Entity/RelatedLinkedDummy.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,27 @@
2121

2222
#[ApiResource()]
2323
#[ApiResource(
24-
uriTemplate: '/secured_dummies/{securedDummyId}/related',
24+
uriTemplate: '/secured_dummies/{securedDummyId}/to_from',
2525
operations: [new GetCollection()],
2626
uriVariables: [
2727
'securedDummyId' => new Link(toProperty: 'securedDummy', fromClass: SecuredDummy::class, security: "is_granted('ROLE_USER') and securedDummy.getOwner() == user"),
2828
]
2929
)]
30+
#[ApiResource(
31+
uriTemplate: '/secured_dummies/{securedDummyId}/with_name',
32+
operations: [new GetCollection()],
33+
uriVariables: [
34+
'securedDummyId' => new Link(toProperty: 'securedDummy', fromClass: SecuredDummy::class, security: "is_granted('ROLE_USER') and testObj.getOwner() == user", securityObjectName: 'testObj'),
35+
]
36+
)]
37+
#[ApiResource(
38+
uriTemplate: '/secured_dummies/{securedDummyId}/related/{id}',
39+
operations: [new GetCollection()],
40+
uriVariables: [
41+
'securedDummyId' => new Link(toProperty: 'securedDummy', fromClass: SecuredDummy::class, security: "is_granted('ROLE_USER') and securedDummy.getOwner() == user"),
42+
'id' => new Link(fromClass: RelatedLinkedDummy::class, security: "is_granted('ROLE_USER') and testObj.getSecuredDummy().getOwner() == user", securityObjectName: 'testObj'),
43+
]
44+
)]
3045
#[Entity]
3146
class RelatedLinkedDummy
3247
{

tests/Fixtures/TestBundle/Entity/SecuredDummy.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Metadata\GraphQl\Mutation;
2121
use ApiPlatform\Metadata\GraphQl\Query;
2222
use ApiPlatform\Metadata\GraphQl\QueryCollection;
23+
use ApiPlatform\Metadata\Link;
2324
use ApiPlatform\Metadata\Post;
2425
use ApiPlatform\Metadata\Put;
2526
use Doctrine\Common\Collections\ArrayCollection;
@@ -33,6 +34,13 @@
3334
* @author Kévin Dunglas <[email protected]>
3435
*/
3536
#[ApiResource(operations: [new Get(security: 'is_granted(\'ROLE_USER\') and object.getOwner() == user'), new Put(securityPostDenormalize: 'is_granted(\'ROLE_USER\') and previous_object.getOwner() == user'), new GetCollection(security: 'is_granted(\'ROLE_USER\') or is_granted(\'ROLE_ADMIN\')'), new GetCollection(uriTemplate: 'custom_data_provider_generator', security: 'is_granted(\'ROLE_USER\')'), new Post(security: 'is_granted(\'ROLE_ADMIN\')')], graphQlOperations: [new Query(name: 'item_query', security: 'is_granted(\'ROLE_ADMIN\') or (is_granted(\'ROLE_USER\') and object.getOwner() == user)'), new QueryCollection(name: 'collection_query', security: 'is_granted(\'ROLE_ADMIN\')'), new Mutation(name: 'delete'), new Mutation(name: 'update', securityPostDenormalize: 'is_granted(\'ROLE_USER\') and previous_object.getOwner() == user'), new Mutation(name: 'create', security: 'is_granted(\'ROLE_ADMIN\')', securityMessage: 'Only admins can create a secured dummy.')], security: 'is_granted(\'ROLE_USER\')')]
37+
#[ApiResource(
38+
uriTemplate: '/related_linked_dummies/{relatedDummyId}/from_from',
39+
operations: [new GetCollection()],
40+
uriVariables: [
41+
'relatedDummyId' => new Link(fromProperty: 'securedDummy', fromClass: RelatedLinkedDummy::class, security: "is_granted('ROLE_USER') and relatedDummy.getSecuredDummy().getOwner() == user", securityObjectName: 'relatedDummy'),
42+
]
43+
)]
3644
#[ORM\Entity]
3745
class SecuredDummy
3846
{

0 commit comments

Comments
 (0)