Hiding fields based on another field and authentication with laravel lighthouse - laravel

I have a case where I need to hide field values on a Type based on another field but also show it in some cases based on the context.
So for example, I have a Type like such:
type Profile {
id: ID! #cacheKey
"Display name of the profile"
display_name: String!
"Handle / Slug of the profile"
handle: Handle
"If the profile is private"
private: Boolean!
"Connected socials this profile has added"
socials: [ProfileSocial!]! #hasMany(relation: "publicSocials") #cache
}
Now I want to hide the socials field if private is true, however, I also want it to be visible if:
You are the owner of this profile (user_id)
You are an administrator
You are friends with the user
I've looked into solving this by using Model Policies and scopes, but I haven't really found a good approach for this.
What I ended up doing was creating a FieldMiddleware which returns null based on few variables, for example:
public function handleField(FieldValue $fieldValue, Closure $next)
{
$fieldValue = $next($fieldValue);
$resolver = $fieldValue->getResolver();
$fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver){
if($root->private) {
$authenticated = auth()->check();
if($authenticated && auth()->id() !== $root->user_id || ! $authenticated) {
return null;
}
}
return $resolver($root, $args, $context, $resolveInfo);
});
return $fieldValue;
}
But I'm afraid this might be a bad approach, so what is the best way to solve this problem?
Tried using Model Policies but it affects hidden attributes which is not supported by Lighthouse.
Tried using scopes but I can't get the models attributes before the builder so I can't conditionally hide fields.
Tried using a field middleware but might be a bad approach.

Related

Translations with Lighthouse GraphQL

I'm using lighthouse on laravel to create APIs for a portal for which I only deal with the development of the backend.
Basically I have to extract a list from a table in the db and so far everything is ok: in the schema file I define the type and the query itself
type StandardLibrary #guard{
id: ID!
code: String!
title: String!
....
}
type Query{
...
standardLibraries: [StandardLibrary!] #all
...
}
At this point, however, I need to get the translations of the title field from the dedicated json files, and I should have solved it by making a #translate directive that I call next to the field that interests me and implemented as follows
type StandardLibrary #guard{
id: ID!
code: String!
title: String! #translate
....
}
namespace App\GraphQL\Directives;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class TranslateDirective extends BaseDirective implements FieldMiddleware
{
public static function definition(): string
{
return /** #lang GraphQL */ <<<'GRAPHQL'
directive #example on FIELD_DEFINITION
GRAPHQL;
}
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$resolver = $fieldValue->getResolver();
// If you have any work to do that does not require the resolver arguments, do it here.
// This code is executed only once per field, whereas the resolver can be called often.
$fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
// Do something before the resolver, e.g. validate $args, check authentication
// Call the actual resolver
$result = $resolver($root, $args, $context, $resolveInfo);
// Do something with the result, e.g. transform some fields
return __($result,[],'en');
});
// Keep the chain of adding field middleware going by calling the next handler.
// Calling this before or after ->setResolver() allows you to control the
// order in which middleware is wrapped around the field.
return $next($fieldValue);
}
}
It works and if it doesn't find the corresponding translation it returns the contents of the field in the db.
But my problem is: how do I dynamically give the language in the query? I've tried declaring a custom client directive but I can't figure out how to implement it, could someone help me out? At some point I'll also have to fetch from the json other fields (such as the description) not present in the db, so I'd need to fetch the translations via the record id and not directly looking for the column content.
You can either do it based on client locale (using the Accept-Language header), or ask API client to explicitly specify expected locales, or even mix both a choose the first-one as a fallback of the second one.
For the header part, I would recommend a Laravel middleware that would simply set the app()->setLocale() based on available locales, and header values.
For the argument, schema would look like this :
title(locale: string): String! #translate
(Yes, argument can exist at any level, not only Query/Mutation)
Value is retrieved as following on directive class:
return __($result,[],$args['locale'] ?? app()->getLocale());

GraphQL define implicitly what a user can do

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?
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.
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
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")
}

Laravel Lighthouse restrict mutation field

I would like to protect some specific fields of a content type to only allow admin user to modify the value but allow users to access it.
Imagine for instance the User type with a is_admin field. Only admin should be able to update it but everyone should be able to read it.
type User {
id: ID!
name: String!
email: String!
is_admin: Boolean!
}
The can directive doesn't seem to work with field in mutation. At first I tried adding #can(ability: "setAdmin") with a custom policy but it didn't had any effect. That same can/policy used on the mutation "worked" but this was not granular enough.
It appears that custom field restrictions using a custom directive should help, but this too doesn't seem to work on a field level in a mutation input type.
type mutation {
updateUser(
input: UpdateUserInput! #spread
): User #update #middleware(checks: ["auth:api"])
}
input UpdateUserInput {
id: ID!
name: String!
email: String!
is_admin: Boolean! #adminOnly
}
With this custom directive in app/GraphQL/Directives/AdminOnlyDirective.php
<?php
namespace App\GraphQL\Directives;
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 AdminOnlyDirective extends BaseDirective implements FieldMiddleware, DefinedDirective
{
/**
* Name of the directive as used in the schema.
*
* #return string
*/
public function name(): string
{
return 'adminOnly';
}
public static function definition(): string
{
return /** #lang GraphQL */ <<<GRAPHQL
"""
Limit field update to only admin.
"""
directive #adminOnly() on FIELD_DEFINITION
GRAPHQL;
}
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) {
$user = $context->user();
if (
// Unauthenticated users don't get to see anything
! $user
// The user's role has to match have the required role
|| !$user->is_admin
) {
return null;
}
return $originalResolver($root, $args, $context, $resolveInfo);
}
)
);
}
}
So, is there a way to prevent "update" of specific fields with laravel lighthouse?
For now, you can use https://lighthouse-php.com/4.16/custom-directives/argument-directives.html#argtransformerdirective to transform that field to null before inserting the database or just throw error out to avoid changes on your specific field, it's like how the #trim behaves;
In lighthouse v5, it's class ArgTransformerDirective has renamed to ArgSanitizerDirective and method transform to sanitize
https://github.com/nuwave/lighthouse/blob/v5.0-alpha.3/src/Schema/Directives/TrimDirective.php
Extra:
I'm still figuring how #can works, cause i still need to drop the whole attribute instead of passing null to my database;
Update: #can only apply to input type instead of input type
The first idea I have in mind here is to create two different inputs and/or mutations. E.g. for admins with access to the field:
updateUserAsAdmin(
input: UpdateUserFullInput! #spread
): User #update
#middleware(checks: ["auth:api"])
#can("users.update.full")
And UpdateUserFullInput contains the is_admin field.
I also came across this discussion a few times: https://github.com/nuwave/lighthouse/issues/325
Maybe you can also find some useful ideas here.
You may also want to look at the official docs: https://github.com/nuwave/lighthouse/blob/master/docs/master/security/authorization.md#custom-field-restrictions

Complex Laravel, Graphql and Lighhouse implementation

My question relates to how to built complex custom resolvers, and why they dont play along well with built in resolvers. I cannot find any good examples on complex resolvers, and my real life case is even more complex than this example.
I have the following schema
type Query {
users: [User!]! #field(resolver: "App\\Library\\UserController#fetchAll")
posts: [Post!]! #field(resolver: "App\\Library\\PostController#fetchAll")
post(id: Int! #eq): Post #find
}
type User {
id: ID!
name: String
posts: [Post!]! #field(resolver: "App\\Library\\PostController#fetchAll")
}
type Post {
id: ID!
content: String!
comments: [Comment] #field(resolver: "App\\Library\\CommentController#fetchAll")
}
type Comment {
id: ID!
reply: String!
commentRating: [CommentRating] #field(resolver: “App\\Library\\CommentRatingController#fetchSum")
}
type CommentRating {
id: ID!
rating: String
}
And for instance I have this query
{
users {
id,
name
posts {
title
comments {
id,
reply
}
}
}
}
I need custom resolvers, because of business logic, but not for all of them. The above works(I use custom resolvers on purpose for all of them, I’ll explain in a bit) but only if I build my
eloquent query in the first resolver that gets called, correctly. Like so
// Function in custom resolver. All other custom resolver which are accessed can just pass the $rootValue on, or operate on it.
public function fetchAll($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
{
// We have some more sophisticated logic to dynamically build the array parameter on the line below, because the query may not always request comments, which means 'posts.comments' wont be needed. As this is the entrypoint, $rootValue is empty
$t = User::with['posts', 'posts.comments', 'posts.comments.ratings'])->get();
// Business logic modules called here
return $t;
}
If I start with a custom resolver, but something in the query uses a built-in resolver, for instance, if change
type User {
id: ID!
name: String
posts: [Post!]! #field(resolver: "App\\Library\\PostController#fetchAll")
}
to
type User {
id: ID!
name: String
posts: [Post!]! #all
}
Then it still runs correctly but the N+1 issue gets introduced. So I can see in my mysql log that multiple queries are being run all of a sudden, where that does not happen if I have only custom, or only built in resolvers. Is it bad practice to let a custom resolver call a built-in resolver?
Is it best to just stick to custom resolvers for all of my types? And is my approach to build the custom resolver the way I do correct? (refer to the public function fetchAll codesnippet)
you can map your resolver class in your schema like
type Query {
users: [User] #field(resolver: "App\\GraphQL\\Queries\\User#FooFunction")
}
and generate this query resolver class with this cammand:
php artisan lighthouse:query User
and put every query you like that on this function called FooFunction:
<?php
namespace App\GraphQL\Queries;
use Carbon\Carbon;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Facades\DB;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class User
{
/**
* Return a value for the field.
*
* #param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* #param mixed[] $args The arguments that were passed into the field.
* #param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context Arbitrary data that is shared between all fields of a single query.
* #param \GraphQL\Type\Definition\ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
* #return mixed
*/
public function FooFunction($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
{
return
DB::table('...')
->where(...)
->get();
}
}
Lighthouse provide directives like #hasOne, #hasMany, #belongsTo which prevent N+1 problem. All you need to do is use them.
https://lighthouse-php.com/master/eloquent/relationships.html#avoiding-the-n-1-performance-problem
Bit of a late answer but the problem was that I did not completely understand the graphql model. The custom resolvers purely need to send back a model of the correct type so that the schema can understand it. My resolvers wasn't returning with the correct types.

Save and Update in a controller together in Yii

There are two tables in my db, users and profile. Profile has user_id as a primary key. Every user can have only one profile. When I upload a image file its name is stored in profile table with that user_id. When there are other fields to be updated in profile table, I first check whether there is already a record with that user_id. In my Profile model I have written
public function checkForSaveOrUpdate()
{
return self::model()->findByAttributes(array('user_id'=>Yii::app()->user->id));
}
and my controller file looks something like this
public function actionCreateInfo()
{
$profile = new Profile;
$profile->user_id = Yii::app()->user->id;
if(isset($_POST['Profile']))
{
if($profile->checkForSaveOrUpdate() === null)
{
$profile->attributes = $_POST['Profile'];
if($profile->save())
Yii::app()->user->setFlash('success','Profile has been saved successfully');
}
elseif($profile = $profile->checkForSaveOrUpdate())
{
$profile->attributes = $_POST['Profile'];
if($profile->update())
Yii::app()->user->setFlash('success','Profile has been updated successfully');
}
$this->redirect(array('index'));
}
$this->render('createInfo',array('profile'=>$profile));
}
My problem is when I already have a record in database,in profile, and I submit a new form the old data is all deleted and only the current values submitted are updated, whereas it should keep the old values and only update the new ones.
If you instaciate the model like:
$model = new YourModel;
you will have the $model->isNewRecord set to true:
var_dump($model->isNewRecord); // true, in this case you use $model->save()
When you find a record, the same property will have the opposite value:
$model = YourModel::model()->findByPk(1);
var_dump($model->isNewRecord); // false - and now you use $model->update(), instead.
Change your function to static function
public static function checkForSaveOrUpdate()
{
return self::model()->findByAttributes(array('user_id'=>Yii::app()->user->id));
}
Then modify action as
public function actionCreateInfo()
{
$profile = Profile::checkForSaveOrUpdate();
if($profile===null)
{
$profile=new Profile;
$profile->user_id = Yii::app()->user->id;
}
if(isset($_POST['Profile']))
{
$profile->attributes = $_POST['Profile'];
if($profile->save())
Yii::app()->user->setFlash('success','Profile has been saved successfully');
$this->redirect(array('index'));
}
$this->render('createInfo',array('profile'=>$profile));
}
Your POST data probably includes all model attributes, including those left blank by the user set to the empty string; the empty string is an accepted value for massive assignment unless otherwise stated in the model rules; a massive assignment is what you actually do with $profile->attributes = $_POST['Profile'];.
One solution would be to unset those attributes that you don't want to update, e.g. those containing an empty string, in the controller.
But this kind of rule should be defined in the model and triggered by calling the validate() method, which you are now skipping by calling update(). You better call save() because internally calls validate() as opposed to update().
A rule for a default value is defined like this:
array(
'attr_name',
'default',
'setOnEmpty' => true,
'value' => null
)

Resources