Skip to content

Commit

Permalink
Merge pull request #202 from moufmouf/inject_user
Browse files Browse the repository at this point in the history
Adding @InjectUser annotation
  • Loading branch information
moufmouf committed Dec 26, 2019
2 parents db286e5 + 34ca966 commit 0299e9e
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 3 deletions.
24 changes: 24 additions & 0 deletions docs/annotations_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ to access it (according to the `@Logged` and `@Right` annotations).

`@HideIfUnauthorized` and `@FailWith` are mutually exclusive.

## @InjectUser annotation

Use the `@InjectUser` annotation to inject an instance of the current user logged in into a parameter of your
query / mutation / field.

**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.

Attribute | Compulsory | Type | Definition
---------------|------------|--------|--------
*for* | *yes* | string | The name of the PHP parameter

## @Security annotation

The `@Security` annotation can be used to check fin-grained access rights.
It is very flexible: it allows you to pass an expression that can contains custom logic.

See [the fine grained security page](fine-grained-security.md) for more details.

**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`.

Attribute | Compulsory | Type | Definition
---------------|------------|--------|--------
*default* | *yes* | string | The security expression

## @Factory annotation

The `@Factory` annotation is used to declare a factory that turns GraphQL input types into objects.
Expand Down
33 changes: 33 additions & 0 deletions docs/authentication_authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,39 @@ class UserController
}
```

## Injecting the current user as a parameter

Use the `@InjectUser` annotation to get an instance of the current user logged in.

```php
namespace App\Controller;

use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Annotations\InjectUser;

class ProductController
{
/**
* @Query
* @InjectUser(for="$user")
* @return Product
*/
public function product(int $id, User $user): Product
{
// ...
}
}
```

The `@InjectUser` annotation can be used next to:

* `@Query` annotations
* `@Mutation` annotations
* `@Field` annotations

The object injected as the current user depends on your framework. It is in fact the object returned by the
["authentication service" configured in GraphQLite](implementing-security.md).

## Hiding fields / queries / mutations

By default, a user analysing the GraphQL schema can see all queries/mutations/types available.
Expand Down
40 changes: 40 additions & 0 deletions src/Annotations/InjectUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Annotations;

use BadMethodCallException;
use function ltrim;

/**
* Use this annotation to tell GraphQLite to inject the current logged user as an input parameter.
* If the parameter is not nullable, the user MUST be logged to access the resource.
*
* @Annotation
* @Target({"METHOD", "ANNOTATION"})
* @Attributes({
* @Attribute("for", type = "string")
* })
*/
class InjectUser implements ParameterAnnotationInterface
{
/** @var string */
private $for;

/**
* @param array<string, mixed> $values
*/
public function __construct(array $values)
{
if (! isset($values['for'])) {
throw new BadMethodCallException('The @InjectUser annotation must be passed a target. For instance: "@InjectUser(for="$user")"');
}
$this->for = ltrim($values['for'], '$');
}

public function getTarget(): string
{
return $this->for;
}
}
12 changes: 11 additions & 1 deletion src/Annotations/ParameterAnnotations.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public function __construct(array $annotations)
/**
* Return annotations of the $className type
*
* @return array<int, ParameterAnnotationInterface>
* @param class-string<T> $className
*
* @return array<int, T>
*
* @template T of ParameterAnnotationInterface
*/
public function getAnnotationsByType(string $className): array
{
Expand All @@ -39,6 +43,12 @@ public function getAnnotationsByType(string $className): array

/**
* Returns at most 1 annotation of the $className type.
*
* @param class-string<T> $className
*
* @return T|null
*
* @template T of ParameterAnnotationInterface
*/
public function getAnnotationByType(string $className): ?ParameterAnnotationInterface
{
Expand Down
1 change: 0 additions & 1 deletion src/Mappers/Parameters/ContainerParameterHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public function __construct(ContainerInterface $container)
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
{
$autowire = $parameterAnnotations->getAnnotationByType(Autowire::class);
assert($autowire instanceof Autowire || $autowire === null);

if ($autowire === null) {
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
Expand Down
39 changes: 39 additions & 0 deletions src/Mappers/Parameters/InjectUserParameterHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Mappers\Parameters;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use ReflectionParameter;
use TheCodingMachine\GraphQLite\Annotations\InjectUser;
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
use TheCodingMachine\GraphQLite\Parameters\InjectUserParameter;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;

/**
* Maps the current user to a parameter targetted by the \@InjectUser annotation.
*/
class InjectUserParameterHandler implements ParameterMiddlewareInterface
{
/** @var AuthenticationServiceInterface */
private $authenticationService;

public function __construct(AuthenticationServiceInterface $authenticationService)
{
$this->authenticationService = $authenticationService;
}

public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
{
$injectUser = $parameterAnnotations->getAnnotationByType(InjectUser::class);

if ($injectUser === null) {
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
}

return new InjectUserParameter($this->authenticationService);
}
}
1 change: 0 additions & 1 deletion src/Mappers/Parameters/TypeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock,
}

$useInputType = $parameterAnnotations->getAnnotationByType(UseInputType::class);
assert($useInputType instanceof UseInputType || $useInputType === null);
if ($useInputType !== null) {
try {
$type = $this->typeResolver->mapNameToInputType($useInputType->getInputType());
Expand Down
33 changes: 33 additions & 0 deletions src/Parameters/InjectUserParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Parameters;

use GraphQL\Type\Definition\ResolveInfo;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;

/**
* A parameter filled from the current user.
*/
class InjectUserParameter implements ParameterInterface
{
/** @var AuthenticationServiceInterface */
private $authenticationService;

public function __construct(AuthenticationServiceInterface $authenticationService)
{
$this->authenticationService = $authenticationService;
}

/**
* @param array<string, mixed> $args
* @param mixed $context
*
* @return mixed
*/
public function resolve(?object $source, array $args, $context, ResolveInfo $info)
{
return $this->authenticationService->getUser();
}
}
2 changes: 2 additions & 0 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\GlobTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterHandler;
use TheCodingMachine\GraphQLite\Mappers\Parameters\InjectUserParameterHandler;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewarePipe;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ResolveInfoParameterHandler;
Expand Down Expand Up @@ -365,6 +366,7 @@ public function createSchema(): Schema
}
$parameterMiddlewarePipe->pipe(new ResolveInfoParameterHandler());
$parameterMiddlewarePipe->pipe(new ContainerParameterHandler($this->container));
$parameterMiddlewarePipe->pipe(new InjectUserParameterHandler($authenticationService));

$fieldsBuilder = new FieldsBuilder(
$annotationReader,
Expand Down
15 changes: 15 additions & 0 deletions tests/Annotations/HideParameterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace TheCodingMachine\GraphQLite\Annotations;

use BadMethodCallException;
use PHPUnit\Framework\TestCase;

class HideParameterTest extends TestCase
{
public function testException(): void
{
$this->expectException(BadMethodCallException::class);
new HideParameter([]);
}
}
15 changes: 15 additions & 0 deletions tests/Annotations/InjectUserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace TheCodingMachine\GraphQLite\Annotations;

use BadMethodCallException;
use PHPUnit\Framework\TestCase;

class InjectUserTest extends TestCase
{
public function testException(): void
{
$this->expectException(BadMethodCallException::class);
new InjectUser([]);
}
}
12 changes: 12 additions & 0 deletions tests/Fixtures/Integration/Controllers/SecurityController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@


use Porpaginas\Arrays\ArrayResult;
use stdClass;
use TheCodingMachine\GraphQLite\Annotations\FailWith;
use TheCodingMachine\GraphQLite\Annotations\InjectUser;
use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Annotations\Security;
Expand Down Expand Up @@ -69,6 +71,16 @@ public function getSecretUsingThis(string $secret): string
return 'you can see this secret only if isAllowed() returns true';
}

/**
* @Query()
* @InjectUser(for="$user")
* @param stdClass $user
*/
public function getInjectedUser(stdClass $user): int
{
return $user->bar;
}

public function isAllowed(string $secret): bool
{
return $secret === '42';
Expand Down
45 changes: 45 additions & 0 deletions tests/Integration/EndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\GlobTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterHandler;
use TheCodingMachine\GraphQLite\Mappers\Parameters\InjectUserParameterHandler;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ResolveInfoParameterHandler;
use TheCodingMachine\GraphQLite\Mappers\PorpaginasTypeMapper;
Expand Down Expand Up @@ -222,6 +223,9 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
ContainerParameterHandler::class => function(ContainerInterface $container) {
return new ContainerParameterHandler($container, true, true);
},
InjectUserParameterHandler::class => function(ContainerInterface $container) {
return new InjectUserParameterHandler($container->get(AuthenticationServiceInterface::class));
},
'testService' => function() {
return 'foo';
},
Expand All @@ -233,6 +237,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
$parameterMiddlewarePipe = new ParameterMiddlewarePipe();
$parameterMiddlewarePipe->pipe(new ResolveInfoParameterHandler());
$parameterMiddlewarePipe->pipe($container->get(ContainerParameterHandler::class));
$parameterMiddlewarePipe->pipe($container->get(InjectUserParameterHandler::class));

return $parameterMiddlewarePipe;
}
Expand Down Expand Up @@ -1313,4 +1318,44 @@ public function testEndToEndMagicFieldWithPhpType(): void
]
], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']);
}

public function testEndToEndInjectUser(): void
{
$container = $this->createContainer([
AuthenticationServiceInterface::class => static function() {
return new class implements AuthenticationServiceInterface {
public function isLogged(): bool
{
return true;
}

public function getUser(): ?object
{
$user = new stdClass();
$user->bar = 42;
return $user;
}
};
}
]);

/**
* @var Schema $schema
*/
$schema = $container->get(Schema::class);

// Test with failWith attribute
$queryString = '
query {
injectedUser
}
';

$result = GraphQL::executeQuery(
$schema,
$queryString
);

$this->assertSame(42, $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['data']['injectedUser']);
}
}

0 comments on commit 0299e9e

Please sign in to comment.