Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entityの書き込みに対するScope制御 #162

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading