Skip to content

Adding @Security annotation #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"webmozart/assert": "^1.4",
"symfony/cache": "^4.3",
"thecodingmachine/cache-utils": "^1",
"ocramius/package-versions": "^1.4"
"ocramius/package-versions": "^1.4",
"symfony/expression-language": "^4"
},
"require-dev": {
"phpunit/phpunit": "^8.2.4",
Expand Down
63 changes: 7 additions & 56 deletions docs/authentication_authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ sidebar_label: Authentication and authorization
You might not want to expose your GraphQL API to anyone. Or you might want to keep some queries/mutations or fields
reserved to some users.

GraphQLite offers some control over what a user can do with your API based on authentication (whether the user
is logged or not) or authorization (what rights the user have).
GraphQLite offers some control over what a user can do with your API. You can restrict access to resources:

- based on authentication using the [`@Logged` annotation](#logged-and-right-annotations) (restrict access to logged users)
- based on authorization using the [`@Right` annotation](#logged-and-right-annotations) (restrict access to logged users with certain rights).
- based on fine-grained authorization using the [`@Security` annotation](fine-grained-security.md) (restrict access for some given resources to some users).

<div class="alert alert-info">
GraphQLite does not have its own security mechanism.
Unless you're using our Symfony Bundle, it is up to you to connect this feature to your framework's security mechanism.<br>
See <a href="#connecting-graphqlite-to-your-framework-s-security-module">Connecting GraphQLite to your framework's security module</a>.
Unless you're using our Symfony Bundle or our Laravel package, it is up to you to connect this feature to your framework's security mechanism.<br>
See <a href="implementing-security.md">Connecting GraphQLite to your framework's security module</a>.
</div>

## `@Logged` and `@Right` annotations
Expand Down Expand Up @@ -112,55 +115,3 @@ While this is the most secured mode, it can have drawbacks when working with dev
(you need to be logged as admin to fetch the complete schema).

<div class="alert alert-info">The "HideIfUnauthorized" mode was the default mode in GraphQLite 3 and is optionnal from GraphQLite 4+.</div>

## Connecting GraphQLite to your framework's security module

<div class="alert alert-info">
This step is NOT necessary for user using GraphQLite through the Symfony Bundle
</div>

GraphQLite needs to know if a user is logged or not, and what rights it has.
But this is specific of the framework you use.

To plug GraphQLite to your framework's security mechanism, you will have to provide two classes implementing:

* `TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface`
* `TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface`

Those two interfaces act as adapters between GraphQLite and your framework:

```php
interface AuthenticationServiceInterface
{
/**
* Returns true if the "current" user is logged.
*
* @return bool
*/
public function isLogged(): bool;
}
```

```php
interface AuthorizationServiceInterface
{
/**
* Returns true if the "current" user has access to the right "$right".
*
* @param string $right
* @return bool
*/
public function isAllowed(string $right): bool;
}
```

You need to write classes that implement these interfaces. Then, you must register those classes with GraphQLite.
It you are [using the `SchemaFactory`](other-frameworks.md), you can register your classes using:

```php
// Configure an authentication service (to resolve the @Logged annotations).
$schemaFactory->setAuthenticationService($myAuthenticationService);
// Configure an authorization service (to resolve the @Right annotations).
$schemaFactory->setAuthorizationService($myAuthorizationService);
```

170 changes: 170 additions & 0 deletions docs/fine-grained-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
id: fine-grained-security
title: Fine grained security
sidebar_label: Fine grained security
---

If the [`@Logged` and `@Right` annotations](authentication_authorization.md#logged-and-right-annotations) are not
granular enough for your needs, you can use the advanced `@Security` annotation.

Using the `@Security` annotation, you can write an *expression* that can contain custom logic. For instance:

- Check that a user can access a given resource
- Check that a user has one right or another right
- ...

## Using the @Security annotation

The `@Security` annotation is very flexible: it allows you to pass an expression that can contains custom logic:

```php
use TheCodingMachine\GraphQLite\Annotations\Security;

// ...

/**
* @Query
* @Security("is_granted('ROLE_ADMIN') or is_granted('POST_SHOW', post)")
*/
public function getPost(Post $post): array
{
// ...
}
```

The *expression* defined in the `@Security` annotation must conform to [Symfony's Expression Language syntax](https://symfony.com/doc/4.4/components/expression_language/syntax.html)

<div class="alert alert-info">
If you are a Symfony user, you might already be used to the <code>@Security</code> annotation. Most of the inspiration
of this annotation comes from Symfony. Warning though! GraphQLite's <code>@Security</code> annotation and
Symfony's <code>@Security</code> annotation are slightly different. Especially, the two annotations do not live
in the same namespace!
</div>

## The `is_granted` function

Use the `is_granted` function to check if a user has a special right.

```php
@Security("is_granted('ROLE_ADMIN')")
```

is similar to

```php
@Right("ROLE_ADMIN")
```

In addition, the `is_granted` function accepts a second optional parameter: the "scope" of the right.

```php
/**
* @Query
* @Security("is_granted('POST_SHOW', post)")
*/
public function getPost(Post $post): array
{
// ...
}
```

In the example above, the `getPost` method can be called only if the logged user has the 'POST_SHOW' permission on the
`$post` object. You can notice that the `$post` object comes from the parameters.

## Accessing method parameters

All parameters passed to the method can be accessed in the `@Security` expression.

```php
/**
* @Query
* @Security("startDate < endDate", statusCode=400, message="End date must be after start date")
*/
public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
{
// ...
}
```

In the example above, we tweak a bit the Security annotation purpose to do simple input validation.

## Setting HTTP code and error message

You can use the `statusCode` and `message` attributes to set the HTTP code and GraphQL error message.

```php
/**
* @Query
* @Security("is_granted('POST_SHOW', post)", statusCode=404, message="Post not found (let's pretend the post does not exists!)")
*/
public function getPost(Post $post): array
{
// ...
}
```

Note: since a single GraphQL call contain many errors, 2 errors might have conflicting HTTP status code.
The resulting status code is up to the GraphQL middleware you use. Most of the time, the status code with the
higher error code will be returned.

## Accessing the user

You can use the `user` variable to access the currently logged user.
You can use the `is_logged()` function to check if a user is logged or not.

```php
/**
* @Query
* @Security("is_logged() && user.age > 18")
*/
public function getNSFWImages(): array
{
// ...
}
```

## Accessing the current object

You can use the `this` variable to access any (public) property / method of the current class.

```php
class Post {
/**
* @Query
* @Security("this.canAccess(post, user)")
*/
public function getPost(Post $post): array
{
// ...
}

public function canAccess(Post $post, User $user): bool
{
// Some custom logic here
}
}
```

## Available scope

The `@Security` annotation can be used in any query, mutation or field, so anywhere you have a `@Query`, `@Mutation`
or `@Field` annotation.

## How to restrict access to a given resource

The `is_granted` method can be used to restrict access to a specific resource.

```php
@Security("is_granted('POST_SHOW', post)")
```

If you are wondering how to configure these fine-grained permissions, this is not something that GraphQLite handles
itself. Instead, this depends on the framework you are using.

If you are using Symfony, you will [create a custom voter](https://symfony.com/doc/current/security/voters.html).

If you are using Laravel, you will [create a Gate or a Policy](https://laravel.com/docs/6.x/authorization).

If you are using another framework, you need to know that the `is_granted` function simply forwards the call to
the `isAllowed` method of the configured `AuthorizationSerice`. See [Connecting GraphQLite to your framework's security module
](implementing-security.md) for more details
58 changes: 58 additions & 0 deletions docs/implementing-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
id: implementing-security
title: Connecting GraphQLite to your framework's security module
sidebar_label: Connecting security to your framework
---

<div class="alert alert-info">
This step is NOT necessary for users using GraphQLite through the Symfony Bundle or the Laravel package
</div>

GraphQLite needs to know if a user is logged or not, and what rights it has.
But this is specific of the framework you use.

To plug GraphQLite to your framework's security mechanism, you will have to provide two classes implementing:

* `TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface`
* `TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface`

Those two interfaces act as adapters between GraphQLite and your framework:

```php
interface AuthenticationServiceInterface
{
/**
* Returns true if the "current" user is logged
*/
public function isLogged(): bool;

/**
* Returns an object representing the current logged user.
* Can return null if the user is not logged.
*/
public function getUser(): ?object;
}
```

```php
interface AuthorizationServiceInterface
{
/**
* Returns true if the "current" user has access to the right "$right"
*
* @param mixed $subject The scope this right applies on. $subject is typically an object or a FQCN. Set $subject to "null" if the right is global.
*/
public function isAllowed(string $right, $subject = null): bool;
}
```

You need to write classes that implement these interfaces. Then, you must register those classes with GraphQLite.
It you are [using the `SchemaFactory`](other_frameworks.md), you can register your classes using:

```php
// Configure an authentication service (to resolve the @Logged annotations).
$schemaFactory->setAuthenticationService($myAuthenticationService);
// Configure an authorization service (to resolve the @Right annotations).
$schemaFactory->setAuthorizationService($myAuthorizationService);
```

Loading