Skip to content

Commit

Permalink
Entityの書き込みに対するScope制御
Browse files Browse the repository at this point in the history
  • Loading branch information
kiy0taka committed Sep 28, 2023
1 parent 53c179c commit e32487d
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 24 deletions.
49 changes: 37 additions & 12 deletions Doctrine/EventSubscriber/EntityListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,72 @@
namespace Plugin\Api42\Doctrine\EventSubscriber;

use Doctrine\Common\EventSubscriber;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreRemoveEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use GraphQL\Error\Error;
use Plugin\Api42\GraphQL\EntityAccessPolicy;
use Plugin\Api42\Service\WebHookEvents;

class EntityListener implements EventSubscriber
{
/**
* @var WebHookEvents
*/
private $webHookEvents;
private WebHookEvents $webHookEvents;

private EntityAccessPolicy $accessPolicy;

/**
* EntityListener constructor.
* @param WebHookEvents $webHookEvents
*/
public function __construct(WebHookEvents $webHookEvents)
public function __construct(WebHookEvents $webHookEvents, EntityAccessPolicy $accessPolicy)
{
$this->webHookEvents = $webHookEvents;
$this->accessPolicy = $accessPolicy;
}

public function getSubscribedEvents()
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::preUpdate,
Events::preRemove,
Events::postPersist,
Events::postUpdate,
Events::preRemove,
];
}

public function postPersist(LifecycleEventArgs $args)
public function prePersist(PrePersistEventArgs $args): void
{
$this->validateEntityScope($args->getObject());
}

public function preUpdate(PreUpdateEventArgs $args): void
{
$this->validateEntityScope($args->getObject());
}

public function postPersist(LifecycleEventArgs $args): void
{
$this->webHookEvents->onCreated($args->getObject());
}

public function postUpdate(LifecycleEventArgs $args)
public function postUpdate(LifecycleEventArgs $args): void
{
$this->webHookEvents->onUpdated($args->getObject());
}

public function preRemove(LifecycleEventArgs $args)
public function preRemove(PreRemoveEventArgs $args): void
{
$this->validateEntityScope($args->getObject());
$this->webHookEvents->onDeleted($args->getObject());
}

private function validateEntityScope(object $entity): void
{
$entityClass = get_class($entity);
if ($this->accessPolicy->canWriteEntity($entityClass) === false) {
throw new Error("Cannot write entity. `{$entityClass}`");
}
}
}
48 changes: 36 additions & 12 deletions GraphQL/EntityAccessPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Eccube\Request\Context;
use Eccube\Security\SecurityContext;
use Symfony\Component\HttpFoundation\RequestStack;

class EntityAccessPolicy
{
Expand All @@ -26,28 +27,23 @@ class EntityAccessPolicy

private Context $requestContext;

public function __construct(SecurityContext $securityContext, Context $requestContext)
private RequestStack $requestStack;

public function __construct(SecurityContext $securityContext, Context $requestContext, RequestStack $requestStack)
{
$this->securityContext = $securityContext;
$this->requestContext = $requestContext;
$this->requestStack = $requestStack;
}

public function canReadEntity(string $entityClass): bool
{
if (is_null($this->securityContext->getLoginUser())) {
return !empty(array_filter($this->frontAllowLists, function (AllowList $al) use ($entityClass) {
return $al->isAllowed($entityClass);
}));
}

// TODO 管理画面をAPIで実装するまでは管理者画面URL以下でアクセスした場合はすべてのEntityを許可する
if ($this->requestContext->isAdmin()) {
return true;
}

$role = 'ROLE_OAUTH2_READ:'.strtoupper((new \ReflectionClass($entityClass))->getShortName());

return $this->securityContext->isGranted($role);
return $this->canAccessEntity($entityClass, false);
}

public function canReadProperty(string $entityClass, $fieldName): bool
Expand All @@ -64,13 +60,41 @@ public function canReadProperty(string $entityClass, $fieldName): bool
return !empty($allowed);
}

public function addAllowList(AllowList $allowList)
public function canWriteEntity(string $entityClass): bool
{
if (!$this->isApiRequest()) {
return true;
}

return $this->canAccessEntity($entityClass, true);
}

private function canAccessEntity(string $entityClass, bool $write): bool
{
if (is_null($this->securityContext->getLoginUser())) {
return !empty(array_filter($this->frontAllowLists, function (AllowList $al) use ($entityClass) {
return $al->isAllowed($entityClass);
}));
}

$access = $write ? 'WRITE' : 'READ';
$role = "ROLE_OAUTH2_{$access}:".strtoupper((new \ReflectionClass($entityClass))->getShortName());

return $this->securityContext->isGranted($role);
}
public function addAllowList(AllowList $allowList): void
{
$this->allowLists[] = $allowList;
}

public function addFrontAllowList(AllowList $allowList)
public function addFrontAllowList(AllowList $allowList): void
{
$this->frontAllowLists[] = $allowList;
}

private function isApiRequest(): bool
{
$mainRequest = $this->requestStack->getMainRequest();
return $mainRequest != null && $mainRequest->getPathInfo() === '/api';
}
}
3 changes: 3 additions & 0 deletions Tests/Web/ApiControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public function permissionProvider()
[['read:Customer'], '{ customer(id:1) { id, password } }', 'Cannot query field "password" on type "Customer".'],
[null, '{ product(id:1) { id, name } }'],
[null, '{ product(id:1) { id, name, Creator { id } } }', 'Cannot query field "Creator" on type "Product".'],
[['read:ProductClass'], 'mutation { updateProductStock(code: "sand-01", stock: 10, stock_unlimited:false) { id } }', 'Cannot write entity. `Eccube\\Entity\\ProductClass`'],
[['read:ProductClass', 'write:ProductClass'], 'mutation { updateProductStock(code: "sand-01", stock: 10, stock_unlimited:false) { id } }', 'Cannot write entity. `Eccube\\Entity\\ProductStock`'],
[['read:ProductClass', 'write:ProductClass', 'write:ProductStock'], 'mutation { updateProductStock(code: "sand-01", stock: 10, stock_unlimited:false) { id } }'],
];
}

Expand Down

0 comments on commit e32487d

Please sign in to comment.