Skip to content

Commit

Permalink
Merge branch 'master' into annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
mcg-web committed Dec 19, 2018
2 parents 1fe0350 + 6d7de3d commit f9bd917
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
env: SYMFONY_VERSION=4.0.*
- php: 7.2
env: SYMFONY_VERSION=4.1.*
- php: 7.2
- php: 7.3
env: SYMFONY_VERSION=4.2.*
- php: nightly
env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs
Expand Down
36 changes: 36 additions & 0 deletions docs/definitions/expression-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,42 @@ All definition config entries can use expression language but it must be explici

[For more details on expression syntax](http://symfony.com/doc/current/components/expression_language/syntax.html)

Private services
----------------

It is not possible to use private services with `service` or `serv` functions since this is equivalent to call
`get` method on the container. Private services must be tag as global variable to be accessible.

Yaml example:

```yaml
App\MyPrivateService:
public: false
tags:
- { name: overblog_graphql.global_variable, alias: my_private_service }
```
To use a vendor private services:
```php
<?php

$vendorPrivateServiceDef = $container->findDefinition(\Vendor\PrivateService::class);
$vendorPrivateServiceDef->addTag('overblog_graphql.global_variable', ['alias' => 'vendor_private_service']);
```

Usage:

```yaml
MyType:
type: object
config:
fields:
name:
type: String!
resolve: '@=my_private_service.formatName(value)'
```
Custom expression function
--------------------------
Expand Down
63 changes: 46 additions & 17 deletions src/Resolver/AccessResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,18 @@ public function resolve(callable $accessChecker, callable $resolveCallback, arra
return $this->checkAccessForStrictMode($accessChecker, $resolveCallback, $resolveArgs);
}

$result = \call_user_func_array($resolveCallback, $resolveArgs);
$resultOrPromise = \call_user_func_array($resolveCallback, $resolveArgs);

if ($result instanceof Promise) {
$result = $result->adoptedPromise;
}

if ($this->promiseAdapter->isThenable($result) || $result instanceof SyncPromise) {
return $this->promiseAdapter->then(
new Promise($result, $this->promiseAdapter),
if ($this->isThenable($resultOrPromise)) {
return $this->createPromise(
$resultOrPromise,
function ($result) use ($accessChecker, $resolveArgs) {
return $this->processFilter($result, $accessChecker, $resolveArgs);
}
);
}

return $this->processFilter($result, $accessChecker, $resolveArgs);
return $this->processFilter($resultOrPromise, $accessChecker, $resolveArgs);
}

private static function isMutationRootField(ResolveInfo $info): bool
Expand All @@ -55,12 +51,21 @@ private static function isMutationRootField(ResolveInfo $info): bool

private function checkAccessForStrictMode(callable $accessChecker, callable $resolveCallback, array $resolveArgs = [])
{
if (!$this->hasAccess($accessChecker, $resolveArgs)) {
$exceptionClassName = self::isMutationRootField($resolveArgs[3]) ? UserError::class : UserWarning::class;
throw new $exceptionClassName('Access denied to this field.');
}
$promiseOrHasAccess = $this->hasAccess($accessChecker, $resolveArgs);
$callback = function ($hasAccess) use ($resolveArgs, $resolveCallback) {
if (!$hasAccess) {
$exceptionClassName = self::isMutationRootField($resolveArgs[3]) ? UserError::class : UserWarning::class;
throw new $exceptionClassName('Access denied to this field.');
}

return \call_user_func_array($resolveCallback, $resolveArgs);
};

return \call_user_func_array($resolveCallback, $resolveArgs);
if ($this->isThenable($promiseOrHasAccess)) {
return $this->createPromise($promiseOrHasAccess, $callback);
} else {
return $callback($promiseOrHasAccess);
}
}

private function processFilter($result, $accessChecker, $resolveArgs)
Expand Down Expand Up @@ -88,11 +93,35 @@ function (Edge $edge) use ($accessChecker, $resolveArgs) {
return $result;
}

private function hasAccess(callable $accessChecker, array $resolveArgs = [], $object = null): bool
private function hasAccess(callable $accessChecker, array $resolveArgs = [], $object = null)
{
$resolveArgs[] = $object;
$access = (bool) \call_user_func_array($accessChecker, $resolveArgs);
$accessOrPromise = \call_user_func_array($accessChecker, $resolveArgs);

return $accessOrPromise;
}

private function isThenable($object): bool
{
$object = $this->extractAdoptedPromise($object);

return $this->promiseAdapter->isThenable($object) || $object instanceof SyncPromise;
}

return $access;
private function extractAdoptedPromise($object)
{
if ($object instanceof Promise) {
$object = $object->adoptedPromise;
}

return $object;
}

private function createPromise($promise, callable $onFulfilled = null): Promise
{
return $this->promiseAdapter->then(
new Promise($this->extractAdoptedPromise($promise), $this->promiseAdapter),
$onFulfilled
);
}
}
5 changes: 5 additions & 0 deletions tests/Functional/App/Resolver/ConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ public function resolveQuery()

return $this->allUsers[0];
}

public function resolvePromiseFullFilled($value)
{
return $this->promiseAdapter->createFulfilled($value);
}
}
4 changes: 4 additions & 0 deletions tests/Functional/App/config/access/mapping/access.types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ User:
access: "@=hasRole('ROLE_ADMIN')"
resolve: ['ROLE_USER']
isEnabled:
# access as a promise
access: "@=resolver('promise', [args['hasAccess']])"
args:
hasAccess: {type: Boolean!}
type: Boolean
resolve: true
friends:
Expand Down
1 change: 1 addition & 0 deletions tests/Functional/App/config/connection/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ services:
- { name: "overblog_graphql.resolver", alias: "node", method: "resolveNode" }
- { name: "overblog_graphql.resolver", alias: "query", method: "resolveQuery" }
- { name: "overblog_graphql.resolver", alias: "connection", method: "resolveConnection" }
- { name: "overblog_graphql.resolver", alias: "promise", method: "resolvePromiseFullFilled" }
40 changes: 39 additions & 1 deletion tests/Functional/Security/AccessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AccessTest extends TestCase

private $userRolesQuery = 'query { user { roles } }';

private $userIsEnabledQuery = 'query { user { isEnabled } }';
private $userIsEnabledQuery = 'query ($hasAccess: Boolean = true) { user { isEnabled(hasAccess: $hasAccess) } }';

private $userFriendsQuery = <<<'EOF'
query {
Expand Down Expand Up @@ -68,6 +68,44 @@ public function testCustomClassLoaderNotRegister(): void
$this->assertResponse($this->userNameQuery, [], static::ANONYMOUS_USER, 'access');
}

public function testNotAuthenticatedUserAccessAsPromisedFulfilledTrue(): void
{
$this->assertResponse(
$this->userIsEnabledQuery,
['data' => ['user' => ['isEnabled' => true]]],
static::ANONYMOUS_USER,
'access'
);
}

public function testNotAuthenticatedUserAccessAsPromisedFulfilledFalse(): void
{
$this->assertResponse(
$this->userIsEnabledQuery,
[
'data' => [
'user' => [
'isEnabled' => null,
],
],
'extensions' => [
'warnings' => [
[
'message' => 'Access denied to this field.',
'category' => 'user',
'locations' => [['line' => 1, 'column' => 45]],
'path' => ['user', 'isEnabled'],
],
],
],
],
static::ANONYMOUS_USER,
'access',
'',
['hasAccess' => false]
);
}

public function testNotAuthenticatedUserAccessToUserName(): void
{
$expected = [
Expand Down

0 comments on commit f9bd917

Please sign in to comment.