Comments (1)
I handle it in our project with these classes, I need to find time to setup the PR and Tests 😅
Bonus: skipOnNull
, I handle also a case that if resolved result is null this will prevent to run authorize()
on null
<?php
namespace App\GraphQL\Directives;
use App\Enums\CanFailAction;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Auth\CanDirective as BaseCanDirective;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Execution\Resolved;
use Nuwave\Lighthouse\Execution\ResolveInfo;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\SoftDeletes\ForceDeleteDirective;
use Nuwave\Lighthouse\SoftDeletes\RestoreDirective;
use Nuwave\Lighthouse\SoftDeletes\TrashedDirective;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Utils;
class CanDirective extends BaseCanDirective
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Check a Laravel Policy to ensure the current user is authorized to access a field.
When `injectArgs` and `args` are used together, the client given
arguments will be passed before the static args.
"""
directive @can(
"""
The ability to check permissions for.
"""
ability: String!
"""
Check the policy against the model instances returned by the field resolver.
Only use this if the field does not mutate data, it is run before checking.
Mutually exclusive with `query`, `find`, and `root`.
"""
resolved: Boolean! = false
"""
Specify the class name of the model to use.
This is only needed when the default model detection does not work.
"""
model: String
"""
Pass along the client given input data as arguments to `Gate::check`.
"""
injectArgs: Boolean! = false
"""
Statically defined arguments that are passed to `Gate::check`.
You may pass arbitrary GraphQL literals,
e.g.: [1, 2, 3] or { foo: "bar" }
"""
args: CanArgs
"""
Action to do if the user is not authorized.
"""
actionOnFail: CanFailAction! = EXCEPTION_NOT_AUTHORIZED
"""
Custom exception class to throw when the user is not authorized, required when
`actionOnFail` is set to `EXCEPTION_CUSTOM`.
"""
customFailException: String
"""
Prevent the field authorizer from running when the resolved value is null.
"""
skipOnNull: Boolean! = false
"""
Query for specific model instances to check the policy against, using arguments
with directives that add constraints to the query builder, such as `@eq`.
Mutually exclusive with `resolved`, `find`, and `root`.
"""
query: Boolean! = false
"""
Apply scopes to the underlying query.
"""
scopes: [String!]
"""
If your policy checks against specific model instances, specify
the name of the field argument that contains its primary key(s).
You may pass the string in dot notation to use nested inputs.
Mutually exclusive with `resolved`, `query`, and `root`.
"""
find: String
"""
Should the query fail when the models of `find` were not found?
"""
findOrFail: Boolean! = true
"""
If your policy should check against the root value.
Mutually exclusive with `resolved`, `query`, and `find`.
"""
root: Boolean! = false
) repeatable on FIELD_DEFINITION
"""
Any constant literal value: https://graphql.github.io/graphql-spec/draft/#sec-Input-Values
"""
scalar CanArgs
GRAPHQL;
}
/** Ensure the user is authorized to access this field. */
public function handleField(FieldValue $fieldValue): void
{
$ability = $this->directiveArgValue('ability');
$resolved = $this->directiveArgValue('resolved');
$skipOnNull = $this->directiveArgValue('skipOnNull', false);
$actionOnFail = CanFailAction::fromName(
$this->directiveArgValue('actionOnFail', CanFailAction::EXCEPTION_NOT_AUTHORIZED)
);
$customFailException = $this->directiveArgValue('customFailException');
if ($actionOnFail === CanFailAction::EXCEPTION_CUSTOM) {
throw_unless($customFailException, new DefinitionException('customFailException is required when actionOnFail is set to EXCEPTION_CUSTOM'));
throw_unless(class_exists($customFailException), new DefinitionException("Class {$customFailException} does not exist."));
}
$fieldValue->wrapResolver(fn (callable $resolver): \Closure => function (mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($customFailException, $actionOnFail, $skipOnNull, $resolver, $ability, $resolved) {
$gate = $this->gate->forUser($context->user());
$checkArguments = $this->buildCheckArguments($args);
if ($resolved) {
return Resolved::handle(
$resolver($root, $args, $context, $resolveInfo),
function ($modelLike) use ($customFailException, $actionOnFail, $skipOnNull, $gate, $ability, $checkArguments) {
$modelOrModels = $modelLike instanceof Paginator
? $modelLike->items()
: $modelLike;
try {
Utils::applyEach(function (?Model $model) use ($skipOnNull, $gate, $ability, $checkArguments): void {
if ($model === null && $skipOnNull) {
return;
}
$this->authorize($gate, $ability, $model, $checkArguments);
}, $modelOrModels);
} catch (AuthorizationException $e) {
return match ($actionOnFail) {
CanFailAction::EXCEPTION_NOT_AUTHORIZED => throw $e,
CanFailAction::EXCEPTION_NOT_FOUND => throw new Error(new ModelNotFoundException()),
CanFailAction::RETURN_NULL => null,
CanFailAction::RETURN_ZERO => 0,
CanFailAction::RETURN_FALSE => false,
CanFailAction::RETURN_EMPTY_STRING => '',
CanFailAction::EXCEPTION_CUSTOM => throw new $customFailException(),
};
}
return $modelLike;
},
);
}
try {
foreach ($this->modelsToCheck($root, $args, $context, $resolveInfo) as $model) {
if ($model === null && $skipOnNull) {
continue;
}
$this->authorize($gate, $ability, $model, $checkArguments);
}
} catch (AuthorizationException $e) {
return match ($actionOnFail) {
CanFailAction::EXCEPTION_NOT_AUTHORIZED => throw $e,
CanFailAction::EXCEPTION_NOT_FOUND => throw new Error(new ModelNotFoundException()),
CanFailAction::RETURN_NULL => null,
CanFailAction::RETURN_ZERO => 0,
CanFailAction::RETURN_FALSE => false,
CanFailAction::RETURN_EMPTY_STRING => '',
CanFailAction::EXCEPTION_CUSTOM => throw new $customFailException(),
};
}
return $resolver($root, $args, $context, $resolveInfo);
});
}
/**
* @param string|array<string> $ability
* @param array<int, mixed> $arguments
*/
protected function authorize(
Gate $gate, string|array $ability,
string|Model|null $model,
array $arguments,
): void {
// The signature of the second argument `$arguments` of `Gate::check`
// should be [modelClassName, additionalArg, additionalArg...]
array_unshift($arguments, $model);
Utils::applyEach(
static function ($ability) use ($gate, $arguments): void {
$response = $gate->inspect($ability, $arguments);
if ($response->denied()) {
throw new AuthorizationException($response->message(), $response->code());
}
},
$ability,
);
}
/**
* @param array<string, mixed> $args
* @return iterable<\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>>
*/
protected function modelsToCheck(mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): iterable
{
if ($this->directiveArgValue('query')) {
return $resolveInfo
->enhanceBuilder(
$this->getModelClass()::query(),
$this->directiveArgValue('scopes', []),
$root,
$args,
$context,
$resolveInfo,
)
->get();
}
if ($this->directiveArgValue('root')) {
return [$root];
}
if ($find = $this->directiveArgValue('find')) {
$findValue = Arr::get($args, $find)
?? throw self::missingKeyToFindModel($find);
$queryBuilder = $this->getModelClass()::query();
$argumentSetDirectives = $resolveInfo->argumentSet->directives;
$directivesContainsForceDelete = $argumentSetDirectives->contains(
Utils::instanceofMatcher(ForceDeleteDirective::class),
);
if ($directivesContainsForceDelete) {
/** @see \Illuminate\Database\Eloquent\SoftDeletes */
// @phpstan-ignore-next-line because it involves mixins
$queryBuilder->withTrashed();
}
$directivesContainsRestore = $argumentSetDirectives->contains(
Utils::instanceofMatcher(RestoreDirective::class),
);
if ($directivesContainsRestore) {
/** @see \Illuminate\Database\Eloquent\SoftDeletes */
// @phpstan-ignore-next-line because it involves mixins
$queryBuilder->onlyTrashed();
}
try {
$enhancedBuilder = $resolveInfo->enhanceBuilder(
$queryBuilder,
$this->directiveArgValue('scopes', []),
$root,
$args,
$context,
$resolveInfo,
Utils::instanceofMatcher(TrashedDirective::class),
);
assert($enhancedBuilder instanceof EloquentBuilder);
$modelOrModels = $this->directiveArgValue('findOrFail', true)
? $enhancedBuilder->findOrFail($findValue)
: $enhancedBuilder->find($findValue);
} catch (ModelNotFoundException $modelNotFoundException) {
throw new Error($modelNotFoundException->getMessage());
}
if ($modelOrModels instanceof Model) {
return [$modelOrModels];
}
if ($modelOrModels === null) {
return [];
}
return $modelOrModels;
}
return [$this->getModelClass()];
}
public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode &$parentType): void
{
$this->validateMutuallyExclusiveArguments(['resolved', 'resolvedRoot', 'query', 'find']);
if ($this->directiveHasArgument('resolved') && $parentType->name->value === RootType::MUTATION) {
throw self::resolvedIsUnsafeInMutations($fieldDefinition->name->value);
}
}
public static function resolvedIsUnsafeInMutations(string $fieldName): DefinitionException
{
return new DefinitionException("Do not use @can with `resolved` on mutation {$fieldName}, it is unsafe as the resolver will run before checking permissions. Use `query`, `resolvedRoot`, or `find`.");
}
public static function invalidRootModel(): Error
{
return new Error('The `resolvedRoot` argument can only be used when the root value is a model instance.');
}
}
and the enum
<?php
namespace App\Enums;
enum CanFailAction
{
case EXCEPTION_NOT_AUTHORIZED;
case EXCEPTION_NOT_FOUND;
case EXCEPTION_CUSTOM;
case RETURN_NULL;
case RETURN_ZERO;
case RETURN_FALSE;
case RETURN_EMPTY_STRING;
public static function fromName(string|CanFailAction $name): self
{
if ($name instanceof CanFailAction) {
return $name;
}
foreach (self::cases() as $status) {
if ($name === $status->name) {
return $status;
}
}
throw new \ValueError("$name is not a valid backing value for enum ".self::class);
}
}
from lighthouse.
Related Issues (20)
- Access context in `FieldMiddleware` HOT 3
- CanArgs is defined twice in schema-directives.graphql
- `extend scalar X` directives are lost
- @canFind is missing "model" argument in graphql definition HOT 1
- Allow to customize the unique key for `PaginatedModelsLoader`
- artisan lighthouse:union stub appears to be incorrect HOT 1
- Problem with subscription middleware HOT 2
- An error occurs in the SubscriptionRegistry when sending a subscription event via Subscription::broadcast with Laravel Octane (Swoole) HOT 5
- Laravel v11 support dependancy missing
- Enum Support for Morph Types HOT 1
- Mutation transaction seem not work HOT 4
- Failed to find class App\GraphQl\Queries\TodoQuery in namespaces [] for directive @paginate
- @whereNull and @whereNotNull No directive found!
- GraphQL subscription with Laravel Reverb HOT 3
- input.id in @can(find: ) HOT 1
- Throttle directive sets wrong decay time HOT 4
- Allow GraphQL types named like PHP reserved keywords in federation entity resolver HOT 1
- When executing Multiple Operations GraphQL, if an error occurs in the StartExecution event listener, an error that ignores the number of operations is returned. HOT 1
- Offer Automatic persisted queries hash in the context HOT 3
- Nested Mutation Regression in v6.37.1 HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from lighthouse.