Skip to content

Commit

Permalink
Refactor Generic Auth plugin (#2281)
Browse files Browse the repository at this point in the history
* Refactor Generic Auth plugin

* ..

* Go

* chore(dependencies): updated changesets for modified dependencies

* Hmm

* chore(dependencies): updated changesets for modified dependencies

* Directive name'

* ..

* ..

* Fix..

* New docs

* Trigger again

* Changeset

* Arrays

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and github-actions[bot] committed Aug 26, 2024
1 parent 04cf1b0 commit 70d4d7a
Show file tree
Hide file tree
Showing 8 changed files with 665 additions and 178 deletions.
5 changes: 5 additions & 0 deletions .changeset/@envelop_generic-auth-2281-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@envelop/generic-auth": patch
---
dependencies updates:
- Updated dependency [`@graphql-tools/utils@^10.5.1` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.5.1) (from `^10.0.6`, in `dependencies`)
97 changes: 97 additions & 0 deletions .changeset/sweet-apples-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
'@envelop/generic-auth': major
'@envelop/extended-validation': minor
---

Refactor Generic Auth plugin;

- [BREAKING] - Now `@auth` directive is renamed to `@authenticated`. If you want to keep the old name you can configure the plugin to use the old name.

```ts
useGenericAuth({
// ...
authDirectiveName: 'auth',
});
```

- [BREAKING] - Now `directiveOrExtensionFieldName` is renamed to `authDirectiveName`.

```diff
useGenericAuth({
// ...
- directiveOrExtensionFieldName: 'auth',
+ authDirectiveName: 'auth',
});
```

- Now auth directives support `OBJECT` and `INTERFACE` locations, so you can use the auth directive on types as well.

```graphql
directive @authenticated on OBJECT | INTERFACE

type User @authenticated {
id: ID!
name: String!
}
```

- `validateUser` function does not receive `fieldAuthDirectiveNode` and `fieldAuthExtension` anymore. Instead, it takes `fieldAuthArgs` which is an object that contains the arguments of the auth directive or extension. So you don't need to parse the arguments manually anymore.

```ts
const validateUser: ValidateUserFn = params => {
if (!params.fieldAuthArgs.roles.includes('admin')) {
return createUnauthorizedError(params);
}
};
```

- `validateUser`'s `objectType` parameter is now renamed to `parentType`. And it takes the original composite type instead of the `GraphQLObjectType` instance. Now it can be `GraphQLInterfaceType` as well.

- `validateUser`'s current parameters are now;

```ts
export type ValidateUserFnParams<UserType> = {
/** The user object. */
user: UserType;
/** The field node from the operation that is being validated. */
fieldNode: FieldNode;
/** The parent type which has the field that is being validated. */
parentType: GraphQLObjectType | GraphQLInterfaceType;
/** The auth directive arguments for the type */
typeAuthArgs?: Record<string, any>;
/** The directives for the type */
typeDirectives?: ReturnType<typeof getDirectiveExtensions>;
/** Scopes that type requires */
typeScopes?: string[][];
/** Policies that type requires */
typePolicies?: string[][];
/** The object field */
field: GraphQLField<any, any>;
/** The auth directive arguments for the field */
fieldAuthArgs?: Record<string, any>;
/** The directives for the field */
fieldDirectives?: ReturnType<typeof getDirectiveExtensions>;
/** Scopes that field requires */
fieldScopes?: string[][];
/** Policies that field requires */
fieldPolicies?: string[][];
/** Extracted scopes from the user object */
userScopes: string[];
/** Policies for the user */
userPolicies: string[];
/** The args passed to the execution function (including operation context and variables) **/
executionArgs: ExecutionArgs;
/** Resolve path */
path: ReadonlyArray<string | number>;
};
```

- New directives for role-based auth are added `@requiresScopes` and `@policy` for more granular control over the auth logic.

```graphql
directive @requiresScopes(scopes: [String!]!) on OBJECT | INTERFACE | FIELD_DEFINITION

directive @policy(policy: String!) on OBJECT | INTERFACE | FIELD_DEFINITION
```

Check README for more information.
111 changes: 96 additions & 15 deletions packages/plugins/extended-validation/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import {
visitInParallel,
visitWithTypeInfo,
} from 'graphql';
import { Plugin, TypedSubscriptionArgs } from '@envelop/core';
import {
AsyncIterableIteratorOrValue,
isAsyncIterable,
OnExecuteEventPayload,
OnExecuteHookResult,
OnSubscribeEventPayload,
OnSubscribeHookResult,
Plugin,
} from '@envelop/core';
import { ExtendedValidationRule } from './common.js';

const symbolExtendedValidationRules = Symbol('extendedValidationContext');
Expand All @@ -30,6 +38,12 @@ export const useExtendedValidation = <PluginContext extends Record<string, any>
* Callback that is invoked if the extended validation yields any errors.
*/
onValidationFailed?: OnValidationFailedCallback;
/**
* Reject the execution if the validation fails.
*
* @default true
*/
rejectOnErrors?: boolean;
}): Plugin<PluginContext & { [symbolExtendedValidationRules]?: ExtendedValidationContext }> => {
let schemaTypeInfo: TypeInfo;

Expand Down Expand Up @@ -57,23 +71,33 @@ export const useExtendedValidation = <PluginContext extends Record<string, any>
}
validationRulesContext.rules.push(...options.rules);
},
onSubscribe: buildHandler('subscribe', getTypeInfo, options.onValidationFailed),
onExecute: buildHandler('execute', getTypeInfo, options.onValidationFailed),
onSubscribe: buildHandler(
'subscribe',
getTypeInfo,
options.onValidationFailed,
options.rejectOnErrors !== false,
),
onExecute: buildHandler(
'execute',
getTypeInfo,
options.onValidationFailed,
options.rejectOnErrors !== false,
),
};
};

function buildHandler(
name: 'execute' | 'subscribe',
getTypeInfo: () => TypeInfo | undefined,
onValidationFailed?: OnValidationFailedCallback,
rejectOnErrors = true,
) {
return function handler({
args,
setResultAndStopExecution,
}: {
args: TypedSubscriptionArgs<any>;
setResultAndStopExecution: (newResult: ExecutionResult) => void;
}) {
}: OnExecuteEventPayload<any> | OnSubscribeEventPayload<any>):
| (OnExecuteHookResult<any> & OnSubscribeHookResult<any>)
| void {
// We hook into onExecute/onSubscribe even though this is a validation pattern. The reasoning behind
// it is that hooking right after validation and before execution has started is the
// same as hooking into the validation step. The benefit of this approach is that
Expand Down Expand Up @@ -101,17 +125,74 @@ function buildHandler(
const visitor = visitInParallel(
validationRulesContext.rules.map(rule => rule(validationContext, args)),
);
visit(args.document, visitWithTypeInfo(typeInfo, visitor));

args.document = visit(args.document, visitWithTypeInfo(typeInfo, visitor));

if (errors.length > 0) {
let result: ExecutionResult = {
data: null,
errors,
};
if (onValidationFailed) {
onValidationFailed({ args, result, setResult: newResult => (result = newResult) });
if (rejectOnErrors) {
let result: ExecutionResult = {
data: null,
errors,
};
if (onValidationFailed) {
onValidationFailed({ args, result, setResult: newResult => (result = newResult) });
}
setResultAndStopExecution(result);
} else {
// eslint-disable-next-line no-inner-declarations
function onResult({
result,
setResult,
}: {
result: AsyncIterableIteratorOrValue<ExecutionResult>;
setResult: (result: AsyncIterableIteratorOrValue<ExecutionResult>) => void;
}) {
if (isAsyncIterable(result)) {
// rejectOnErrors is false doesn't work with async iterables
setResult({
data: null,
errors,
});
return;
}
const newResult = {
...result,
errors: [...(result.errors || []), ...errors],
};
function visitPath(path: readonly (string | number)[], data: any = {}) {
let currentData = (data ||= typeof path[0] === 'number' ? [] : {});
for (const pathItemIndex in path.slice(0, -1)) {
const pathItem = path[pathItemIndex];
currentData = currentData[pathItem] ||=
typeof path[Number(pathItemIndex) + 1] === 'number' ||
path[Number(pathItemIndex) + 1]
? []
: {};
if (Array.isArray(currentData)) {
let pathItemIndexInArray = Number(pathItemIndex) + 1;
if (path[pathItemIndexInArray] === '@') {
pathItemIndexInArray = Number(pathItemIndex) + 2;
}
currentData = currentData.map((c, i) =>
visitPath(path.slice(pathItemIndexInArray), c),
);
}
}
currentData[path[path.length - 1]] = null;
return data;
}
errors.forEach(e => {
if (e.path?.length) {
newResult.data = visitPath(e.path, newResult.data);
}
});
setResult(newResult);
}
return {
onSubscribeResult: onResult,
onExecuteDone: onResult,
};
}
setResultAndStopExecution(result);
}
}
}
Expand Down
Loading

0 comments on commit 70d4d7a

Please sign in to comment.