Closed kevinvdburgt closed 6 years ago
@kevinvdburgt Lighthouse could make this process a bit easier, however, currently you could swap out the Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider
(registered here) interface w/ an implementation of your own in your service provider's register
method.
We may want to switch this over to a singleton and expose a setRootPath
method which would allow outside access to swapping out the path if needed.
First of all, switching to Lighthouse: Great idea! I did the same.
I think that multi-schema is most probably a bad idea, and also not trivial to implement on our side. View https://github.com/nuwave/lighthouse/issues/273 for some more discussion around this.
Just throwing a bunch of ideas of how you can solve your use case in a single schema:
@can
or @middleware
to protect fields@adminOnly
to granularly restrict field accessBenefits you get are:
Use @can or @middleware to protect fields
And @group
for multiple queries/mutations at once.
e.g:
extend type Query @group(middleware: ["can:access-functionality"]) {
restrictedQuery1: ...
restrictedQuery2: ...
}
I do that currently and its working fine - but I also do have fields and types I'd like to not expose (while still having introspection for most other parts of the API for 3rd party access).
One idea I had is excluding specific fields and types from introspection... Not sure if this would be efficiently possible without loosing the benefits of caching the AST.
Just a thought...
@jbbr Unless you put sensitive information in the schema itself (which you should not do), i do not see a reason to not expose it. I would consider your whole schema public information, as you are indirectly publishing it anyways through the queries your Frontends contain.
Not a big fan of security through obscurity 😉
Thanks for the replies! This is the first time for me, using a graphql schema file instead of the Folkloreatelier/laravel-graphql way.
How can i make it so, that an admin user (authorized by token) sees a different type and uses a different resolver then a normal user (in this example below). Also, not all our types are coming from a Eloquent model, most of them are from external resources (MongoDB e.d.) - how can we use @paginate
without specifying a model?
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
# Type for users only
type Notification {
id: ID!
name: String!
created_at: DateTime!
}
# Type for admins only
type Notification {
id: ID!
name: String!
description: String
created_at: DateTime!
updated_at: DateTime!
deleted_at: DateTime
}
type Query {
# Resolver: \App\Http\GraphQL\Queries\Notification::class
# For users only
notifications: [Notification!]!
# Resolver: \App\Http\GraphQL\Queries\Admin\Notification::class
# For admins only
notifications: [Notification!]!
}
In your example I would side w/ @spawnia and make a single schema if you're just adding additional fields for admin access. So in this case you would have a single definition for Notification
like so:
type Notification {
id: ID!
name: String!
description: String @admin
created_at: DateTime!
updated_at: DateTime! @admin
deleted_at: DateTime @admin
}
And create a AdminDirective
that only returned the value if the user has access:
namespace App\Http\GraphQL\Directives;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
class AdminDirective implements FieldMiddleware
{
/**
* Name of the directive.
*
* @return string
*/
public function name()
{
return 'admin';
}
/**
* Resolve the field directive.
*
* @param FieldValue $value
* @param \Closure $next
*
* @return FieldValue
*/
public function handleField(FieldValue $value, \Closure $next)
{
$resolver = $value->getResolver();
return $next($value->setResolver(function ($root, $args, $context) use ($resolver) {
if (!$context->user->isAdmin()) {
return null;
}
return call_user_func_array($resolver, func_get_args());
}));
}
}
how can we use
@paginate
without specifying a model?
https://github.com/nuwave/lighthouse/pull/326
How can i make it so, that an admin user (authorized by token) sees a different type and uses a different resolver then a normal user (in this example below).
I typed out my answer, but @chrissm79 beat me to it.
However, I have seen use-cases for multiple schemas where some sort of admin
schema is used to define and query types for internal use only.
For example, I've seen a schema dealing w/ Patient and Doctor records where where certain internal notes, procedure codes, etc were only defined and used in a "admin" schema which basically "extended" the base schema to add the new types and created a new Mutation
and Query
type.
Since it was a completely different endpoint, it made the introspection consistent and didn't expose internal business logic but allowed them to reuse their resolvers for shared types.
@chrissm79 that seems to be working, however, as our GraphQL api is public for some specific user roles, is there a way, that we can hide fields/types/queries e.d. using directives?
@spawnia #326 looks good 👍
@kevinvdburgt It's highly discouraged to hide/show type fields in the same schema. However, with #327 you can create a different schema and use your controller to set the root path of the schema to your public/internal/etc schema files.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider;
use Nuwave\Lighthouse\Support\Http\Controllers\GraphQLController as BaseController;
class GraphQLController extends BaseController
{
/**
* Execute GraphQL query.
*
* @param Request $request
*
* @return Response
*/
public function adminQuery(Request $request)
{
app(SchemaSourceProvider::class)->setRootPath("/path/to/admin-schema.graphql");
return parent::query($request);
}
}
If you had an internal only field on a type defined in your default schema you could extend it:
extend type Notification {
description: String
}
Doing things this way allows you to serve different schema w/ different endpoints which keeps introspection consistent and predictable but also allows you to prevent exposure of internal logic you don't want to expose via your public api.
The best approach is creating a new GraphQL controller for now, as it seems weird to publish admin only queries and mutations to a public API. Where introspection is enabled.
@kevinvdburgt That's very interesting approach🤤
@chrissm79 i tried to make a new controller and give another schema file to it, however, when checking with xdebug, the SchemaSourceProvider's root path has been changed, but the queries are still executed agains the schema defined in config/lighthouse.php
Still unsure, if it is always the best approach to stich all schemas together, like, for example
As i can see, that in some cases stitching all schema's together to a single one is usefull. I still have trouble understanding why splitting it is considered a bad idea:
Having a public GraphQL schema for users with introspection enabled and having a private schema without introspection enabled.
When having just 4 queries/mutations for the user (public) and 200+ queries/mutations for admins only. Isnt it way to noisy in the documentation browser of the paremeters which is optional e.d.
When, for example, authorizing combining admin, users, api's e.d.
# Normal users
mutation {
authorize(username: "foobar" password: "secret" tfa: "2 factor auth code") {
user
}
}
# Admin users
mutation {
authorize(email: "user@example.com" password: "secret" tfa: "2 factor auth code") {
admin
}
}
# API (External services)
mutation {
authorize(apikey: "hello-world-:)") {
user
}
}
As the user and API can be in the same schema ofcourse, but just as an example that the API has different access stuff than an normal user.
Example A
Having a public GraphQL schema for users with introspection enabled and having a private schema without introspection enabled.
I really do not see the point in disabling introspection at all. Unless you do something ridiculous, e.g. put a password in a field description, you are not gaining a security advantage through that.
Example B
When having just 4 queries/mutations for the user (public) and 200+ queries/mutations for admins only. Isnt it way to noisy in the documentation browser of the paremeters which is optional e.d.
I think the example you are setting up is a bit extreme, although i do see the point of it. This might actually be the best reason i have heard yet for multi-schema.
Example C
How about you just name the fields differently then? authorizeAdmin
, authorizeUser
...
Just throwing a bunch of ideas of how you can solve your use case in a single schema:
- Name fields that return different data differently
- Use
@can
or@middleware
to protect fields- Add a custom directive like
@adminOnly
to granularly restrict field accessBenefits you get are:
- Idiomatic single-endpoint server
- DRY schema definition
- Ease of developing and switching around roles
can i use @middleware
like this?
type User {
id: ID! @globalId
name: String
email: String @middleware(checks: ["auth:api"])
}
'route' => [
'middleware' => ['api'],
],
because the email value still show without auth using @middleware
.
or i can use @field
to protect email, only user login can see the email?
type User {
id: ID! @globalId
name: String
email: String @field(resolver: "App\\Http\\GraphQL\\Types\\UserType@email")
}
class UserType
{
public function email($root, array $args)
{
if(auth()->guard('api')->user()->id === $root->id){
return $root->email;
}
return null;
}
}
how to use "can" in field email?
can i use @middleware like this?
You should be able to, yes. Be aware that global middleware and field middleware run at different points of the execution lifecycle and do not have anything to do with each other.
If the field email
can be seen despite the middleware on it, that seems like a bug. If you can provide a failing test case i would be happy to take care of that in Lighthouse.
If the field email can be seen despite the middleware on it. that seems like a bug. If you can provide a failing test case i would be happy to take care of that in Lighthouse.
I don't know how to provide a failing test to you. but this my code
my schema.graphql
type User {
id: ID! @globalId
name: String
email: String @middleware(checks: ["auth:api"])
stage_name: String
phone: String @middleware(checks: ["auth:api"])
}
type AuthToken {
token_type: String!
expires_in: Int!
access_token: String!
}
type LoginPayLoad {
auth_token: AuthToken
user: User
}
type Query {
"Get data profile by login jwt.auth"
me: User @auth
"Single influencer data by argument name and ID"
user(
"You can search by name argument"
name: String @where(operator: "like")
"find by ID"
id: ID @eq
): User @find(model: "App\\User")
}
"Mutation"
type Mutation{
login(
email: String!
password: String!
): LoginPayLoad @field(resolver: "App\\Http\\GraphQL\\Mutations\\AccountMutator@login")
}
lighthouse.php
'route' => [
'prefix' => '',
// 'middleware' => ['web','api'], // [ 'loghttp']
'middleware' => ['api'],
],
AccountMutator.php
class AccountMutator
{
/**
* guard to be used authenticated.
*
* @return \Illuminate\Contracts\Auth\Guard
*/
public function guard()
{
return \Illuminate\Support\Facades\Auth::guard('api');
}
public function login($root, array $args, $context)
{
$credentials = array_only($args,['email','password']);
try {
$access_token = $this->guard()->attempt($credentials);
} catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
return null;
}
$auth_token = [
'token_type' => 'bearer',
'expires_in' => auth()->guard('api')->factory()->getTTL() * 1440,
'access_token' => 'Bearer '.$access_token,
];
//throw_if(array_key_exists('error', $auth_token), new \Exception(array_get($auth_token, 'error')));
$user = $this->guard()->user();
return compact('auth_token', 'user');
}
}
and then i try the query
{
me{
id
name
email
}
user(id:5060){
id
name
email
}
}
and the result without authorization
{
"data": {
"me": null,
"user": {
"id": "SW5mbHVlbmNlcjo1MDYw",
"name": "john doe",
"email": "xyz@gmail.com"
}
}
}
with authorization
{
"data": {
"me": {
"id": "SW5mbHVlbmNlcjo1MDYw",
"name": "john doe",
"email": "xyz@gmail.com"
},
"user": {
"id": "SW5mbHVlbmNlcjo1MDYw",
"name": "john doe",
"email": "xyz@gmail.com"
}
}
}
the field email still can be seen without authorization
and then i try this schema
type User {
id: ID! @globalId
name: String
email: String @middleware(checks: ["jwt.auth"])
stage_name: String
phone: String @middleware(checks: ["auth:api"])
}
kernel.php
protected $routeMiddleware = [
...
'jwt.auth' => \Tymon\JWTAuth\Http\Middleware\Authenticate::class,
...
]
the field email still can be seen
I suggest you check if your auth:api
middleware is being called and if it throws if you are not authenticated.
And make sure you are using the latest version of Lighthouse.
thanks @spawnia after update version from 2.4 to 2.6.1, `@middleware' is working good
Is your feature request related to a problem? Please describe. As we have multiple guards in our Laravel app, so do we have quite some different queries and mutations for each user. Some of them, named the same but gives back different data.
Describe the solution you'd like I would like to split some schema, like
https://example.org/graphql/schama-a
,https://example.org/graphql/schama-b
andhttps://example.org/graphql/schama-c
. With each of them their own resolvers.Describe alternatives you've considered I've been using https://github.com/Folkloreatelier/laravel-graphql for a while now, where you can define multiple schemas per endpoint. But would be nice to have it in this package as well as our team is considering moving to Lighthouse instead.