From 09bd29a62fd903b07d4c72d6ce34c58d993d2b68 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Thu, 20 Jul 2023 12:13:57 -0700 Subject: [PATCH] Add new defaultStrategy option to scope-auth plugin --- .changeset/chilled-squids-enjoy.md | 5 ++++ packages/plugin-scope-auth/README.md | 29 +++++++++++++++++-- .../plugin-scope-auth/src/request-cache.ts | 7 +++-- packages/plugin-scope-auth/src/types.ts | 1 + website/pages/docs/plugins/scope-auth.mdx | 22 ++++++++++++++ 5 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 .changeset/chilled-squids-enjoy.md diff --git a/.changeset/chilled-squids-enjoy.md b/.changeset/chilled-squids-enjoy.md new file mode 100644 index 000000000..66cc1f6df --- /dev/null +++ b/.changeset/chilled-squids-enjoy.md @@ -0,0 +1,5 @@ +--- +'@pothos/plugin-scope-auth': minor +--- + +Add new defaultStrategy option to allow enforcing all scopes in a scopeMap without using `$all` diff --git a/packages/plugin-scope-auth/README.md b/packages/plugin-scope-auth/README.md index 7734cfe25..0b4b880db 100644 --- a/packages/plugin-scope-auth/README.md +++ b/packages/plugin-scope-auth/README.md @@ -365,7 +365,8 @@ For example, if you want to re-throw errors thrown by authorization functions yo writeing a custom `unauthorizedError` callback like this: ```typescript -import SchemaBuilder, { AuthFailure, AuthScopeFailureType } from '@pothos/core'; +import SchemaBuilder from '@pothos/core'; +import ScopeAuthPlugin, { AuthFailure, AuthScopeFailureType } from '@pothos/plugin-scope-auth'; // Find the first error and re-throw it function throwFirstError(failure: AuthFailure) { @@ -380,7 +381,7 @@ function throwFirstError(failure: AuthFailure) { failure.kind === AuthScopeFailureType.AllAuthScopes ) { for (const child of failure.failures) { - throwFirstError(child, recursive); + throwFirstError(child); } } } @@ -397,7 +398,7 @@ const builder = new SchemaBuilder<{ // throw an error if it's found throwFirstError(result.failure); // throw a fallback error if no error was found - new Error(`Not authorized`); + return new Error(`Not authorized`); }, }, plugins: [ScopeAuthPlugin], @@ -578,6 +579,28 @@ above example requires a request to have either the `employee` or `deferredScope `public` scope. `$any` and `$all` each take a scope map as their parameters, and can be nested inside each other. +You can change the default strategy used for top level auth scopes by setting the `defaultStrategy` +option in the builder (defaults to `any`): + +```typescript +const builder = new SchemaBuilder<{ + Context: { + user: User | null; + }; + AuthScopes: { + loggedIn: boolean; + }; +}>({ + plugins: [ScopeAuthPlugin], + scopeAuthOptions: { + defaultStrategy: 'all', + }, + authScopes: async (context) => ({ + loggedIn: !!context.user, + }), +}); +``` + ### Auth that depends on parent value For cases where the required scopes depend on the value of the requested resource you can use a diff --git a/packages/plugin-scope-auth/src/request-cache.ts b/packages/plugin-scope-auth/src/request-cache.ts index f7193e4d8..facbcf1dc 100644 --- a/packages/plugin-scope-auth/src/request-cache.ts +++ b/packages/plugin-scope-auth/src/request-cache.ts @@ -34,12 +34,15 @@ export default class RequestCache { treatErrorsAsUnauthorized: boolean; + defaultStrategy: 'all' | 'any'; + constructor(builder: PothosSchemaTypes.SchemaBuilder, context: Types['Context']) { this.builder = builder; this.context = context; this.cacheKey = builder.options.scopeAuthOptions?.cacheKey; this.treatErrorsAsUnauthorized = builder.options.scopeAuthOptions?.treatErrorsAsUnauthorized ?? false; + this.defaultStrategy = builder.options.scopeAuthOptions?.defaultStrategy ?? 'any'; } static fromContext( @@ -294,7 +297,7 @@ export default class RequestCache { } if ($any) { - const anyResult = this.evaluateScopeMap($any, info); + const anyResult = this.evaluateScopeMap($any, info, false); if (isThenable(anyResult)) { promises.push(anyResult); @@ -374,7 +377,7 @@ export default class RequestCache { evaluateScopeMap( map: AuthScopeMap | boolean, info?: GraphQLResolveInfo, - forAll = false, + forAll = this.defaultStrategy === 'all', ): MaybePromise { if (typeof map === 'boolean') { return map diff --git a/packages/plugin-scope-auth/src/types.ts b/packages/plugin-scope-auth/src/types.ts index fb88ff609..cda022da3 100644 --- a/packages/plugin-scope-auth/src/types.ts +++ b/packages/plugin-scope-auth/src/types.ts @@ -18,6 +18,7 @@ export interface ScopeAuthPluginOptions { runScopesOnType?: boolean; treatErrorsAsUnauthorized?: boolean; authorizeOnSubscribe?: boolean; + defaultStrategy?: 'all' | 'any'; } export interface BuiltInScopes { diff --git a/website/pages/docs/plugins/scope-auth.mdx b/website/pages/docs/plugins/scope-auth.mdx index e9d32b66d..a5d95ce94 100644 --- a/website/pages/docs/plugins/scope-auth.mdx +++ b/website/pages/docs/plugins/scope-auth.mdx @@ -593,6 +593,28 @@ above example requires a request to have either the `employee` or `deferredScope `public` scope. `$any` and `$all` each take a scope map as their parameters, and can be nested inside each other. +You can change the default strategy used for top level auth scopes by setting the `defaultStrategy` +option in the builder (defaults to `any`): + +```typescript +const builder = new SchemaBuilder<{ + Context: { + user: User | null; + }; + AuthScopes: { + loggedIn: boolean; + }; +}>({ + plugins: [ScopeAuthPlugin], + scopeAuthOptions: { + defaultStrategy: 'all', + }, + authScopes: async (context) => ({ + loggedIn: !!context.user, + }), +}); +``` + ### Auth that depends on parent value For cases where the required scopes depend on the value of the requested resource you can use a