Giter VIP home page Giter VIP logo

Comments (1)

exodusanto avatar exodusanto commented on August 25, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.