leocavalcante / siler

⚡ Flat-files and plain-old PHP functions rockin'on as a set of general purpose high-level abstractions.
https://siler.leocavalcante.dev
MIT License
1.12k stars 90 forks source link

GraphQL authorization #419

Open gskierk opened 4 years ago

gskierk commented 4 years ago

Hello,

What is recommended way to add authorization logic to Siler\GraphQL module? I'm aware of that authentication could be successfully done via route middleware, but the same cannot be done with authorization (or shouldn't... to achieve it anyway, you would have to parse request once more and fetch operation type, operation name and arguments recursively...).

For instance, package thecodingmachine/graphqlite offers @Security annotations that are handled by authentication and authorization services that you set in schema factory.

Are there any plans to implement similar feature to Siler\GraphQL? Or maybe solution already exists and I didn't manage to find it in documentation and source code? Maybe with dispatcher...?

I created actually a temporary workaround for this in Query class, but I wouldn't recommend it to anyone...

/**
 * @GraphQL\ObjectType(name="Query")
 */
class Query
{
    /**
     * @GraphQL\Field(name="user", description="Get user")
     * @GraphQL\Args(
     *     {
     *          @GraphQL\Field(name="id", type="Int")
     *     }
     * )
     */
    public static function getUser($root, array $args, $context, ResolveInfo $resolveInfo): User
    {
        return self::secure('_getUser', $args);
    }

    public static function secure(string $method, $arguments)
    {
        $user = \Siler\Container\retrieve(User::class);
        $request = \Siler\GraphQL\request()->toArray();

        $operationType = '...'; // check if operation type is 'query' or 'mutation', etc.
        $permissions = '...'; // load permissions for your role, specific $operationType and $method
        if (!isset($permissions['access']) || $permissions['access'] === false)
        {
            throw new Error('Access denied');
        }

        if (isset($acl['conditions']))
        {
            foreach ($acl['conditions'] as $condition)
            {
                // for example OwnUserCondition::class with method check($user, $args)
                // that verifies if $user->getId() === $args['id']

            }
        }

        return static::__callStatic($method, $arguments);
    }

    public static function _getUser($root, array $args, $context, ResolveInfo $resolveInfo): User
    {
        //...
    }
}
leocavalcante commented 4 years ago

Hi, Actually, since there is no auth in the GraphQL spec, I haven't thought about a Siler provided solution for that, but we can came up with a solution! :)

On my APIs a use JWT then I add an User or a GuestUser to the GraphQL context then on each resolver I see in if there is a User the Context and if the User has proper roles.

gskierk commented 4 years ago

So your solution is conceptually the same as mine. Repeating permission checking on each resolver doesn't sound good for me, that's why I mentioned GraphQLite solution.

Defining authorization logic inside the resolver is fine when learning GraphQL or prototyping. However, for a production codebase, delegate authorization logic to the business logic layer. - Authorization | GraphQL

I don't know the source code of Siler good enough, but I would create new server directive @auth(conditions, message, ...) and corresponding annotation @Authorization(conditions, message, ...) that would add @auth directive to proper places in generated schema. Then, somewhere, where $args, $context, etc. are already accessible (I don't know which Siler\GraphQL function would it be...), I would fetch authorization directive/annotation of current resolver and perform permission checking...

$user = $context['user'] ?? \Siler\Container\get('user'); // or any other way to get current user...
foreach ($authorization->getConditions() as $condition) {
    if (!$condition::check($user, $args)) {
        throw new SomeSuitableException($authorization->getMessage(), ..., $authorization->getSomething());
    }
}
leocavalcante commented 4 years ago

So, following the GraphQL's website recommendations:

Business Logic Layer Your business logic layer should act as the single source of truth for enforcing business domain rules Where should you define the actual business logic? Where should you perform validation and authorization checks? The answer: inside a dedicated business logic layer. Your business logic layer should act as the single source of truth for enforcing business domain rules. In the diagram above, all entry points (REST, GraphQL, and RPC) into the system will be processed with the same validation, authorization, and error handling rules.

Auth shouldn't be on any part of GraphQL solution, it should be in your business rules/domain layer.


Anyway what do think about the Middleware pattern? Then you can define a pipeline and add it to run before reaching the resolver.

gskierk commented 4 years ago

Inside standard middleware I have no direct access to $root, $args, $context, $resolveInfo variables, but middleware specifically for GraphQL would do the job.