-
Hi! I have a need to default to guarded queries/mutations so no data is accidentally accessible publicly if I miss a @guard directive, how do I accomplish this? I'd like something along the lines of this, but am open to other suggestions: type Mutation @guard {
createUser(input: CreateUserInput! @spread): User @create
// this:
login(input: LoginInput!): LoginResponse! @public
// or this:
login(input: LoginInput!): LoginResponse! @guard(with: ['public'])
} The problem with the above syntax is that @guard gets run before @public or @guard(with: ['public']), so my current attempts have failed. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
To answer my own question: I found that in the end, the simplest solution is to write a custom replacement for the The simplest proof of concept for those with the same issue: // app/GraphQL/Directives/ProtectDirective.php
<?php declare(strict_types=1);
namespace App\GraphQL\Directives;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionNode;
use Illuminate\Container\Container;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Nuwave\Lighthouse\Auth\AuthServiceProvider;
use Nuwave\Lighthouse\Exceptions\AuthenticationException;
use Nuwave\Lighthouse\Execution\ResolveInfo;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
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;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
/**
* @see \Illuminate\Auth\Middleware\Authenticate
*/
class ProtectDirective extends BaseDirective implements FieldMiddleware, TypeManipulator, TypeExtensionManipulator
{
public function __construct(
protected AuthFactory $authFactory,
) {}
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
directive @protect(with: [String!]) repeatable on FIELD_DEFINITION | OBJECT
GRAPHQL;
}
public function handleField(FieldValue $fieldValue): void
{
$fieldValue->wrapResolver(fn (callable $resolver): \Closure => function (mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
$guards = $this->directiveArgValue('with', AuthServiceProvider::guards());
$context->setUser($this->authenticate($guards));
return $resolver($root, $args, $context, $resolveInfo);
});
}
/**
* Determine if the user is logged in to any of the given guards.
*
* @param array<string|null> $guards
*/
protected function authenticate(array $guards): Authenticatable
{
foreach ($guards as $guard) {
$user = $this->authFactory->guard($guard)->user();
if ($user !== null) {
// @phpstan-ignore-next-line passing null works fine here
$this->authFactory->shouldUse($guard);
return $user;
}
}
$this->unauthenticated($guards);
}
/**
* Handle an unauthenticated user.
*
* @param array<string|null> $guards
*
* @return never
*/
protected function unauthenticated(array $guards): void
{
throw new AuthenticationException(AuthenticationException::MESSAGE, $guards);
}
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void
{
static::addDirectiveToFields($this->directiveNode, $typeDefinition);
}
public function manipulateTypeExtension(DocumentAST &$documentAST, TypeExtensionNode &$typeExtension): void
{
static::addDirectiveToFields($this->directiveNode, $typeExtension);
}
private static function addDirectiveToFields(DirectiveNode $directiveNode, ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode &$typeWithFields): void
{
$name = $directiveNode->name->value;
$directiveLocator = Container::getInstance()->make(DirectiveLocator::class);
$directive = $directiveLocator->resolve($name);
$directiveDefinition = ASTHelper::extractDirectiveDefinition($directive::definition());
foreach ($typeWithFields->fields as $fieldDefinition) {
// If the field already has the same directive defined, and it is not
// a repeatable directive, skip over it.
// Field directives are more specific than those defined on a type.
if (
ASTHelper::hasDirective($fieldDefinition, $name)
&& ! $directiveDefinition->repeatable
) {
continue;
}
if (! ASTHelper::hasDirective($fieldDefinition, 'public')) {
$fieldDefinition->directives = ASTHelper::prepend($fieldDefinition->directives, $directiveNode);
}
}
}
} Stub // app/GraphQL/Directives/PublicDirective.php
<?php declare(strict_types=1);
namespace App\GraphQL\Directives;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionNode;
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\TypeExtensionManipulator;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
final class PublicDirective extends BaseDirective implements TypeManipulator, TypeExtensionManipulator, FieldMiddleware
{
// TODO implement the directive https://lighthouse-php.com/master/custom-directives/getting-started.html
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
directive @public on OBJECT | FIELD_DEFINITION
GRAPHQL;
}
/**
* Apply manipulations from a type definition node.
*/
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void
{
ASTHelper::addDirectiveToFields($this->directiveNode, $typeDefinition);
}
/**
* Apply manipulations from a type extension node.
*/
public function manipulateTypeExtension(DocumentAST &$documentAST, TypeExtensionNode &$typeExtension): void
{
// TODO implement the type extension manipulator
ASTHelper::addDirectiveToFields($this->directiveNode, $typeExtension);
}
/**
* Wrap around the final field resolver.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue): void
{
// TODO implement the field middleware
}
} |
Beta Was this translation helpful? Give feedback.
To answer my own question:
I found that in the end, the simplest solution is to write a custom replacement for the
@guard
directive that implements logic to skip prepending@guard
to fields if a stub@public
directive is found on the field in question.The simplest proof of concept for those with the same issue: