-1

I'm using Laravel Lighthouse.

Following situation: I have users, and those users should be allowed to access different datasets and run different mutations.

My solution: The users are assigned roles, and those roles define what datasets the user can access and what mutations can be run.

I'm stuck at the implementation. What I could do is to write down all my Queries and Mutations in my schema, and have policies to restrict access to them. What I would prefer is to have a way to see from the schema which role has access to what.

My idea: Have a type for each role, and in that type associate what data can be accessed and what mutations can be run

Here's some example code that might explain what I'm going for, even though the syntax is probably invalid:

type Query {
    me: User @auth
}

type User {
    id: ID
    username: String
    first_name: String
    wage: Float
    password: String

    roles: [Role]
    role(name: String! @eq): Role @find
}

type Role {
    id: ID
    name: String
}

type AdminRole {
    #set of users whose data the admin has access to
    #also directly restrict the amount of attributes that are accessible (e.g. password is not accessible)
    #this is invalid syntax, I know
    users: [Users] @all {
        id
        first_name
        wage
    }
    
    #a mutation the admin has access to
    updateUser(id: ID!, wage: Float): User @update
}

What query I'd like to run for the admin to get all wages:

query {
    me {
        role(name: "AdminRole") {
            users {
                wage
            }
        }
    }
}

What mutation I'd like to run for the admin to update a user's wage:

mutation {
    me {
        role(name: "AdminRole") {
            updateUser(id: 7, wage: 10.00) {
                id
                wage
            }
        }
    }
}

So instead of writing policies that restrict access to things, I'd rather just have everything defined implicitly in the schema. This would make defining and answering "What can an admin do?" more intuitive and easier to comprehend, because it's written down in a single spot, rather than several policies.

I assume this is not possible in the way I described above. What's the closest thing to it? Or are there issues with this approach?

GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
Lutan
  • 43
  • 4

3 Answers3

0

What about @can directive? You can use it on query, input or field. With little modifying can be able to set role instead of permission.

Second idea is serving other schema to different authenticated users based on roles.

0

Take a look at my answer here: https://stackoverflow.com/a/63405046/2397915

I describe there two different approaches for different goals. At the end these two are allowing me to restrict every part of my schema how I want. Works perfectly in role-based setup

lorado
  • 336
  • 1
  • 7
0

At the end I wrote a custom directive, similar to what lorado mentioned, but a tad more simple:

<?php

namespace App\GraphQL\Directives;

use Closure;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator;

class RestrictDirective extends BaseDirective implements FieldMiddleware, TypeExtensionManipulator {
    public function name() {
        return "restrict";
    }

    public static function definition(): string {
        return /** @lang GraphQL */ <<<'SDL'
directive @restrict(
    roles: Mixed!
) on FIELD_DEFINITION | OBJECT
SDL;
    }

    public function handleField(FieldValue $fieldValue, Closure $next): FieldValue {
        $resolver = $fieldValue->getResolver();

        $fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
            //get the passed rights
            $rights = $this->directiveArgValue("rights");
            if ($rights === null) throw new DefinitionException("Missing argument 'rights' for directive '@restrict'.");
            
            //allow both a single string and an array as input
            if (!is_array($rights)) $rights = [$rights];
            
            //current user, must be logged in
            $user = $context->user();
            if (!$user) $this->no();

            //returns an array of strings
            $user_rights = $user->getAllRightNames();
            
            //this is the part where we check whether the user has the rights or not
            if (empty(array_intersect($user_rights, $rights))) $this->no();

            return $resolver($root, $args, $context, $resolveInfo);
        });

        return $next($fieldValue);
    }

    public function no() {
        throw new AuthorizationException("You are not authorized to access {$this->nodeName()}");
    }

    public function manipulateTypeExtension(DocumentAST &$documentAST, TypeExtensionNode &$typeExtension) {
        ASTHelper::addDirectiveToFields($this->directiveNode, $typeExtension);
    }
}

used as such:

type User {
    id: ID!
    username: String
    
    extraPayments: [ExtraPayment] @restrict(rights: ["baseWorkingTime", "someOtherRight"])
}

#how to easily restrict a subset of attributes
extend type User @restrict(rights: "baseWorkingTime") {
    wage: Float
    password: String
}

Here, extraPayments are restricted to someone having at least one of the two rights. A whole set of attributes is restricted by restricting an extension. Mutations are restricted the same way, if so desired:

type Mutation {
    test: String @restrict(rights: "baseWorkingTime")
}
Lutan
  • 43
  • 4