问题
In my graphql API I have to authorize requests to fields by two different factors. Whether the user is authorized to access the data or whether the data belongs to the user. For example, the user should be able to see its own user data and all users with admin rights should be able to see this data too. I want to protect the fields, so users with different permisions can access some fields of a type, but don't have access all fields.
I tried to do this with @can
, but I didn't find any way to get the model that is currently accessed. I can just get the model, when is use @can
on a Query or the whole Type.
Creating a directive as in the docs to protect fields with permissions also doesn't fit my needs, as I don't get the model here.
Is there a good way to deal with my authorisation needs?
I'm using Laravel 7 and Lighthouse 4.16.
回答1:
I don't understand your issue for 100%. There are two situations:
- You want to protect a root query/mutation field. For this you can use laravel policy and
@can
directive. Something like this:
type Query {
protectedPost(postId: ID! @eq): Post @find @can(ability: "view", find: "id")
}
In your PostPolicy
:
class PostPolicy
{
//...
public function view(User $user, Post $post)
{
// check if use has access to data
if ($post->author_id === $user->id || $user->role === UserRole::Admin) {
return true;
}
return false;
}
}
Also don't forget to register you policy to the model.
- You want to protect partially fields of your type. E.g. you have a
Post
type like
type Post {
id: ID!
secretAdminComment: String
}
and you want to protect secretAdminComment
. This seems to be a little tricky, but in general you can use @can
directive code and extend it in way you need it. The main logic is like - if user is able to access - use regular field resolver, and if not - return null. I'll give you an example of how I implemented it for my app. In my app users may have multiple roles. Also it is possible to pass a user ID from current/nested field (or model in terms of laravel) to check against an authorized user.
namespace App\GraphQL\Directives;
use App\Enums\UserRole;
use App\User;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class CanAccessDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'SDL'
"""
Checks if user has at least one of the role, or user ID is match the value of path defined in allowForUserIdIn. If there are no matches, returns null instead of regular value
"""
directive @canAccess(
"""
The user roles to check
"""
roles: [String!]
"""
Custom null value
"""
nullValue: Mixed
"""
Define if user assigment should be checked. Currently authanticated user ID will be compared to defined path relative to root.
"""
allowForUserIdIn: String
) on FIELD_DEFINITION
SDL;
}
/**
* @inheritDoc
*/
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$originalResolver = $fieldValue->getResolver();
return $next(
$fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($originalResolver) {
$nullValue = $this->directiveArgValue('nullValue', null);
/** @var User $user */
$user = $context->user();
if (!$user) {
return $nullValue;
}
// check role
$allowedRoles = [];
$roles = $this->directiveArgValue('roles', []);
foreach ($roles as $role) {
try {
$allowedRoles[] = UserRole::getValue($role);
} catch (\Exception $e) {
throw new DefinitionException("Defined role '$role' could not be found in UserRole enum! Consider using only defined roles.");
}
}
$allowedViaRole = count(array_intersect($allowedRoles, $user->roles)) > 0;
// check user assignment
$allowForLinkedUser = false;
$allowForUserIdIn = $this->directiveArgValue('allowForUserIdIn');
if ($allowForUserIdIn !== null) {
$compareToUserId = array_reduce(
explode('.', $allowForUserIdIn),
function ($object, $property) {
if ($object === null || !is_object($object) || !(isset($object->$property))) {
return null;
}
return $object->$property;
},
$root
);
$allowForLinkedUser = $user->id === $compareToUserId;
}
if ($allowedViaRole || $allowForLinkedUser) {
return $originalResolver($root, $args, $context, $resolveInfo);
}
return $nullValue;
}
)
);
}
}
And here is the usage of that directive giving access for certain roles:
type Post {
id: ID!
secretAdminComment: String @canAccess(roles: ["Admin", "Moderator"])
}
Or giving access to user linked to the field. So only user with ID equals to $post->author_id
will be able to get the value:
type Post {
id: ID!
author_id: ID!
secretAdminComment: String @canAccess(allowForUserIdIn: "author_id")
}
And you are also able to combine both parameters, so user gets access if he either has one of the roles, or has the ID that is defined in $post->author_id
.
type Post {
id: ID!
author_id: ID!
secretAdminComment: String @canAccess(roles: ["Admin", "Moderator"], allowForUserIdIn: "author_id")
}
You are also able to define custom null value via nullValue
parameter.
I hope I could help you =)
回答2:
Did you try implementing laravel policies for your model?
https://laravel.com/docs/7.x/authorization#generating-policies
@can should be used along with model policies :)
https://lighthouse-php.com/4.16/api-reference/directives.html#can
来源:https://stackoverflow.com/questions/63359841/authorisation-in-laravel-lighthouse