@pothos/plugin-scope-auth
Advanced tools
Comparing version 3.0.2 to 3.1.0
# Change Log | ||
## 3.1.0 | ||
### Minor Changes | ||
- 4234ab3e: Allow Error customization | ||
## 3.0.2 | ||
@@ -4,0 +10,0 @@ |
@@ -0,5 +1,7 @@ | ||
import { AuthFailure } from '.'; | ||
export declare class ForbiddenError extends Error { | ||
code: string; | ||
constructor(message: string); | ||
result: AuthFailure; | ||
constructor(message: string, result?: AuthFailure); | ||
} | ||
//# sourceMappingURL=errors.d.ts.map |
@@ -0,6 +1,8 @@ | ||
import { AuthScopeFailureType } from './index.js'; | ||
export class ForbiddenError extends Error { | ||
constructor(message) { | ||
constructor(message, result) { | ||
super(message); | ||
this.code = "FORBIDDEN"; | ||
this.name = "ForbiddenError"; | ||
this.result = result !== null && result !== void 0 ? result : { kind: AuthScopeFailureType.Unknown }; | ||
Object.defineProperty(this, "name", { value: "ForbiddenError" }); | ||
@@ -7,0 +9,0 @@ } |
@@ -1,6 +0,4 @@ | ||
import { GraphQLResolveInfo } from 'graphql'; | ||
import { FieldKind, FieldNullability, FieldOptionsFromKind, FieldRef, InputFieldMap, InputShapeFromFields, MaybePromise, Normalize, Resolver, RootName, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; | ||
import { ForbiddenError } from './errors'; | ||
import { FieldKind, FieldNullability, FieldOptionsFromKind, FieldRef, InputFieldMap, InputShapeFromFields, Normalize, Resolver, RootName, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; | ||
import { FieldAuthScopes, FieldGrantScopes, ScopeAuthInitializer, ScopeAuthPluginOptions, TypeAuthScopes, TypeGrantScopes } from './types'; | ||
import { ContextForAuth, PothosScopeAuthPlugin } from '.'; | ||
import { ContextForAuth, PothosScopeAuthPlugin, UnauthorizedOptions } from '.'; | ||
declare global { | ||
@@ -12,3 +10,3 @@ export namespace PothosSchemaTypes { | ||
interface SchemaBuilderOptions<Types extends SchemaTypes> { | ||
scopeAuthOptions?: ScopeAuthPluginOptions; | ||
scopeAuthOptions?: ScopeAuthPluginOptions<Types>; | ||
authScopes: ScopeAuthInitializer<Types>; | ||
@@ -39,7 +37,6 @@ } | ||
} | ||
interface FieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, ResolveShape, ResolveReturnShape> { | ||
interface FieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, ResolveShape, ResolveReturnShape> extends UnauthorizedOptions<Types, ParentShape, Type, Nullable, Args> { | ||
authScopes?: FieldAuthScopes<Types, ParentShape, InputShapeFromFields<Args>>; | ||
grantScopes?: FieldGrantScopes<Types, ParentShape, InputShapeFromFields<Args>>; | ||
skipTypeScopes?: boolean; | ||
unauthorizedResolver?: (parent: ParentShape, args: InputShapeFromFields<Args>, context: Types['Context'], info: GraphQLResolveInfo, error: ForbiddenError) => MaybePromise<ShapeFromTypeParam<Types, Type, Nullable>>; | ||
} | ||
@@ -46,0 +43,0 @@ interface ObjectFieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, ResolveReturnShape> extends FieldOptions<Types, ParentShape, Type, Nullable, Args, ParentShape, ResolveReturnShape> { |
@@ -5,4 +5,4 @@ import './global-types'; | ||
import { ResolveStep } from './types'; | ||
export * from './errors'; | ||
export * from './types'; | ||
export * from './errors'; | ||
declare const pluginName: "scopeAuth"; | ||
@@ -9,0 +9,0 @@ export default pluginName; |
@@ -5,4 +5,4 @@ import './global-types.js'; | ||
import { createFieldAuthScopesStep, createFieldGrantScopesStep, createResolveStep, createTypeAuthScopesStep, createTypeGrantScopesStep, } from './steps.js'; | ||
export * from './errors.js'; | ||
export * from './types.js'; | ||
export * from './errors.js'; | ||
const pluginName = "scopeAuth"; | ||
@@ -9,0 +9,0 @@ export default pluginName; |
import { GraphQLResolveInfo } from 'graphql'; | ||
import { MaybePromise, Path, SchemaTypes } from '@pothos/core'; | ||
import { ScopeLoaderMap } from './types'; | ||
import { PothosScopeAuthPlugin } from '.'; | ||
import { AuthFailure, PothosScopeAuthPlugin } from '.'; | ||
export default class RequestCache<Types extends SchemaTypes> { | ||
plugin: PothosScopeAuthPlugin<Types>; | ||
context: Types["Context"]; | ||
mapCache: Map<{}, MaybePromise<boolean>>; | ||
scopeCache: Map<keyof Types["AuthScopes"], Map<unknown, MaybePromise<boolean>>>; | ||
typeCache: Map<string, Map<unknown, MaybePromise<boolean>>>; | ||
typeGrants: Map<string, Map<unknown, MaybePromise<boolean>>>; | ||
mapCache: Map<{}, MaybePromise<AuthFailure | null>>; | ||
scopeCache: Map<keyof Types["AuthScopes"], Map<unknown, MaybePromise<AuthFailure | null>>>; | ||
typeCache: Map<string, Map<unknown, MaybePromise<AuthFailure | null>>>; | ||
typeGrants: Map<string, Map<unknown, MaybePromise<null>>>; | ||
grantCache: Map<string, Set<string>>; | ||
@@ -18,7 +18,7 @@ scopes?: MaybePromise<ScopeLoaderMap<Types>>; | ||
withScopes<T>(cb: (scopes: ScopeLoaderMap<Types>) => MaybePromise<T>): MaybePromise<T>; | ||
saveGrantedScopes(scopes: string[], path: Path | undefined): boolean; | ||
saveGrantedScopes(scopes: string[], path: Path | undefined): null; | ||
testGrantedScopes(scope: string, path: Path): boolean; | ||
grantTypeScopes(type: string, parent: unknown, info: GraphQLResolveInfo, cb: () => MaybePromise<string[]>): MaybePromise<boolean>; | ||
evaluateScopeLoader<T extends keyof Types['AuthScopes']>(scopes: ScopeLoaderMap<Types>, name: T, arg: Types['AuthScopes'][T]): MaybePromise<boolean>; | ||
grantTypeScopes(type: string, parent: unknown, info: GraphQLResolveInfo, cb: () => MaybePromise<string[]>): Promise<null>; | ||
evaluateScopeLoader<T extends keyof Types['AuthScopes']>(scopes: ScopeLoaderMap<Types>, name: T, arg: Types['AuthScopes'][T]): import("./types").AuthScopeFailure | import("./types").AuthScopeFunctionFailure | import("./types").GrantedScopeFailure | import("./types").AnyAuthScopesFailure | import("./types").AllAuthScopesFailure | import("./types").UnknownAuthFailure | Promise<AuthFailure | null>; | ||
} | ||
//# sourceMappingURL=request-cache.d.ts.map |
import { isThenable } from '@pothos/core'; | ||
import { cacheKey } from './util.js'; | ||
import { AuthScopeFailureType } from './index.js'; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
@@ -50,3 +51,3 @@ const requestCache = new WeakMap(); | ||
} | ||
return true; | ||
return null; | ||
} | ||
@@ -90,3 +91,21 @@ testGrantedScopes(scope, path) { | ||
} | ||
cache.set(arg, loader(arg)); | ||
const result = loader(arg); | ||
if (isThenable(result)) { | ||
cache.set(arg, result.then((r) => r | ||
? null | ||
: { | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: name, | ||
parameter: arg, | ||
})); | ||
} | ||
else { | ||
cache.set(arg, result | ||
? null | ||
: { | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: name, | ||
parameter: arg, | ||
}); | ||
} | ||
} | ||
@@ -93,0 +112,0 @@ return cache.get(arg); |
@@ -5,8 +5,9 @@ import { isThenable } from '@pothos/core'; | ||
import ResolveState from './resolve-state.js'; | ||
const defaultUnauthorizedResolver = (root, args, context, info, error) => { | ||
const defaultUnauthorizedResolver = (_root, _args, _context, _info, error) => { | ||
throw error; | ||
}; | ||
export function resolveHelper(steps, plugin, fieldConfig) { | ||
var _a; | ||
var _a, _b, _c, _d; | ||
const unauthorizedResolver = (_a = fieldConfig.pothosOptions.unauthorizedResolver) !== null && _a !== void 0 ? _a : defaultUnauthorizedResolver; | ||
const createError = (_d = (_b = fieldConfig.pothosOptions.unauthorizedError) !== null && _b !== void 0 ? _b : (_c = plugin.builder.options.scopeAuthOptions) === null || _c === void 0 ? void 0 : _c.unauthorizedError) !== null && _d !== void 0 ? _d : ((parent, args, context, info, result) => result.message); | ||
return (parent, args, context, info) => { | ||
@@ -20,6 +21,10 @@ const state = new ResolveState(RequestCache.fromContext(context, plugin)); | ||
return stepResult.then((result) => { | ||
if (!result) { | ||
return unauthorizedResolver(parent, args, context, info, new ForbiddenError(typeof errorMessage === "function" | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage)); | ||
if (result) { | ||
const error = createError(parent, args, context, info, { | ||
message: typeof errorMessage === "function" | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
failure: result, | ||
}); | ||
return unauthorizedResolver(parent, args, context, info, typeof error === "string" ? new ForbiddenError(error, result) : error); | ||
} | ||
@@ -29,6 +34,10 @@ return runSteps(i + 1); | ||
} | ||
if (!stepResult) { | ||
return unauthorizedResolver(parent, args, context, info, new ForbiddenError(typeof errorMessage === "function" | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage)); | ||
if (stepResult) { | ||
const error = createError(parent, args, context, info, { | ||
message: typeof errorMessage === "function" | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
failure: stepResult, | ||
}); | ||
return unauthorizedResolver(parent, args, context, info, typeof error === "string" ? new ForbiddenError(error, stepResult) : error); | ||
} | ||
@@ -35,0 +44,0 @@ } |
@@ -5,2 +5,3 @@ import { GraphQLResolveInfo } from 'graphql'; | ||
import { AuthScopeMap, ScopeLoaderMap, TypeAuthScopesFunction } from './types'; | ||
import { AuthFailure } from '.'; | ||
export default class ResolveState<Types extends SchemaTypes> { | ||
@@ -10,6 +11,6 @@ cache: RequestCache<Types>; | ||
constructor(cache: RequestCache<Types>); | ||
evaluateScopeMapWithScopes({ $all, $any, $granted, ...map }: AuthScopeMap<Types>, scopes: ScopeLoaderMap<Types>, info: GraphQLResolveInfo, forAll: boolean): MaybePromise<boolean>; | ||
evaluateScopeMap(map: AuthScopeMap<Types>, info: GraphQLResolveInfo, forAll?: boolean): MaybePromise<boolean>; | ||
evaluateTypeScopeFunction(authScopes: TypeAuthScopesFunction<Types, unknown>, type: string, parent: unknown, info: GraphQLResolveInfo): MaybePromise<boolean>; | ||
evaluateScopeMapWithScopes({ $all, $any, $granted, ...map }: AuthScopeMap<Types>, scopes: ScopeLoaderMap<Types>, info: GraphQLResolveInfo, forAll: boolean): MaybePromise<null | AuthFailure>; | ||
evaluateScopeMap(map: AuthScopeMap<Types> | boolean, info: GraphQLResolveInfo, forAll?: boolean): MaybePromise<null | AuthFailure>; | ||
evaluateTypeScopeFunction(authScopes: TypeAuthScopesFunction<Types, unknown>, type: string, parent: unknown, info: GraphQLResolveInfo): import("./types").AuthScopeFailure | import("./types").AuthScopeFunctionFailure | import("./types").GrantedScopeFailure | import("./types").AnyAuthScopesFailure | import("./types").AllAuthScopesFailure | import("./types").UnknownAuthFailure | Promise<AuthFailure | null>; | ||
} | ||
//# sourceMappingURL=resolve-state.d.ts.map |
import { isThenable } from '@pothos/core'; | ||
import { canCache } from './util.js'; | ||
import { AuthScopeFailureType } from './index.js'; | ||
export default class ResolveState { | ||
@@ -9,7 +10,17 @@ constructor(cache) { | ||
const scopeNames = Object.keys(map); | ||
const problems = []; | ||
const failure = { | ||
kind: forAll ? AuthScopeFailureType.AllAuthScopes : AuthScopeFailureType.AnyAuthScopes, | ||
failures: problems, | ||
}; | ||
const loaderList = []; | ||
for (const scopeName of scopeNames) { | ||
if (scopes[scopeName] == null || scopes[scopeName] === false) { | ||
problems.push({ | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: scopeName, | ||
parameter: map[scopeName], | ||
}); | ||
if (forAll) { | ||
return false; | ||
return failure; | ||
} | ||
@@ -23,5 +34,15 @@ // eslint-disable-next-line no-continue | ||
} | ||
else if (scope === !forAll) { | ||
return scope; | ||
else if (scope && !forAll) { | ||
return null; | ||
} | ||
else if (!scope) { | ||
problems.push({ | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: scopeName, | ||
parameter: map[scopeName], | ||
}); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
} | ||
@@ -31,30 +52,44 @@ const promises = []; | ||
const result = this.cache.testGrantedScopes($granted, info.path); | ||
if (result !== forAll) { | ||
return result; | ||
if (result && !forAll) { | ||
return null; | ||
} | ||
if (!result) { | ||
problems.push({ | ||
kind: AuthScopeFailureType.GrantedScope, | ||
scope: $granted, | ||
}); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
} | ||
if ($any) { | ||
const anyResult = this.evaluateScopeMap($any, info); | ||
if (typeof anyResult === "boolean") { | ||
if (anyResult === !forAll) { | ||
return anyResult; | ||
if (isThenable(anyResult)) { | ||
promises.push(anyResult); | ||
} | ||
else if (anyResult === null && !forAll) { | ||
return null; | ||
} | ||
else if (anyResult) { | ||
problems.push(anyResult); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
else { | ||
promises.push(anyResult); | ||
} | ||
} | ||
if ($all) { | ||
const allResult = this.evaluateScopeMap($all, info, true); | ||
if (typeof allResult === "boolean") { | ||
if (allResult === !forAll) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => allResult); | ||
} | ||
return allResult; | ||
if (isThenable(allResult)) { | ||
promises.push(allResult); | ||
} | ||
else if (allResult === null && !forAll) { | ||
return resolveAndReturn(null); | ||
} | ||
else if (allResult) { | ||
problems.push(allResult); | ||
if (forAll) { | ||
return resolveAndReturn(failure); | ||
} | ||
} | ||
else { | ||
promises.push(allResult); | ||
} | ||
} | ||
@@ -66,15 +101,45 @@ for (const [loaderName, arg] of loaderList) { | ||
} | ||
else if (result === !forAll) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => result); | ||
else if (result === null && !forAll) { | ||
return resolveAndReturn(null); | ||
} | ||
else if (result) { | ||
problems.push(result); | ||
if (forAll) { | ||
return resolveAndReturn(failure); | ||
} | ||
return result; | ||
} | ||
} | ||
if (promises.length === 0) { | ||
return forAll; | ||
return forAll && problems.length === 0 ? null : failure; | ||
} | ||
return Promise.all(promises).then((results) => forAll ? results.every(Boolean) : results.some(Boolean)); | ||
return Promise.all(promises).then((results) => { | ||
let hasSuccess = false; | ||
results.forEach((result) => { | ||
if (result) { | ||
problems.push(result); | ||
} | ||
else { | ||
hasSuccess = true; | ||
} | ||
}); | ||
if (forAll) { | ||
return problems.length > 0 ? failure : null; | ||
} | ||
return hasSuccess ? null : failure; | ||
}); | ||
function resolveAndReturn(val) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => val); | ||
} | ||
return val; | ||
} | ||
} | ||
evaluateScopeMap(map, info, forAll = false) { | ||
if (typeof map === "boolean") { | ||
return map | ||
? null | ||
: { | ||
kind: AuthScopeFailureType.AuthScopeFunction, | ||
}; | ||
} | ||
if (!this.cache.mapCache.has(map)) { | ||
@@ -98,6 +163,6 @@ const result = this.cache.withScopes((scopes) => this.evaluateScopeMapWithScopes(map, scopes, info, forAll)); | ||
if (isThenable(result)) { | ||
cache.set(parent, result.then((resolved) => typeof resolved === "boolean" ? resolved : this.evaluateScopeMap(resolved, info))); | ||
cache.set(parent, result.then((resolved) => this.evaluateScopeMap(resolved, info))); | ||
} | ||
else { | ||
cache.set(parent, typeof result === "boolean" ? result : this.evaluateScopeMap(result, info)); | ||
cache.set(parent, this.evaluateScopeMap(result, info)); | ||
} | ||
@@ -104,0 +169,0 @@ } |
@@ -28,12 +28,4 @@ import { isThenable } from '@pothos/core'; | ||
if (isThenable(scopeMap)) { | ||
return scopeMap.then((resolved) => { | ||
if (typeof resolved === "boolean") { | ||
return resolved; | ||
} | ||
return state.evaluateScopeMap(resolved, info); | ||
}); | ||
return scopeMap.then((resolved) => state.evaluateScopeMap(resolved, info)); | ||
} | ||
if (typeof scopeMap === "boolean") { | ||
return scopeMap; | ||
} | ||
return state.evaluateScopeMap(scopeMap, info); | ||
@@ -54,3 +46,3 @@ }, | ||
state.cache.saveGrantedScopes(grantScopes, info.path); | ||
return true; | ||
return null; | ||
} | ||
@@ -61,7 +53,7 @@ const result = grantScopes(parent, args, context, info); | ||
state.cache.saveGrantedScopes(resolved, info.path); | ||
return true; | ||
return null; | ||
}); | ||
} | ||
state.cache.saveGrantedScopes(result, info.path); | ||
return true; | ||
return null; | ||
}, | ||
@@ -78,7 +70,7 @@ }; | ||
state.resolveValue = resolved; | ||
return true; | ||
return null; | ||
}); | ||
} | ||
state.resolveValue = result; | ||
return true; | ||
return null; | ||
}, | ||
@@ -85,0 +77,0 @@ }; |
import { GraphQLResolveInfo } from 'graphql'; | ||
import { MaybePromise, Merge, SchemaTypes } from '@pothos/core'; | ||
import { FieldNullability, InputFieldMap, InputShapeFromFields, MaybePromise, Merge, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; | ||
import ResolveState from './resolve-state'; | ||
export interface ScopeAuthPluginOptions { | ||
export interface ScopeAuthPluginOptions<Types extends SchemaTypes> { | ||
unauthorizedError?: UnauthorizedErrorFn<Types, unknown, {}>; | ||
} | ||
@@ -21,7 +22,49 @@ export interface BuiltInScopes<Types extends SchemaTypes> { | ||
export declare type FieldGrantScopes<Types extends SchemaTypes, Parent, Args extends {}> = string[] | ((parent: Parent, args: Args, context: Types['Context'], info: GraphQLResolveInfo) => MaybePromise<string[]>); | ||
export declare enum AuthScopeFailureType { | ||
AuthScope = "AuthScope", | ||
AuthScopeFunction = "AuthScopeFunction", | ||
GrantedScope = "GrantedScope", | ||
AnyAuthScopes = "AnyAuthScopes", | ||
AllAuthScopes = "AllAuthScopes", | ||
Unknown = "Unknown" | ||
} | ||
export interface AuthScopeFailure { | ||
kind: AuthScopeFailureType.AuthScope; | ||
scope: string; | ||
parameter: unknown; | ||
} | ||
export interface AuthScopeFunctionFailure { | ||
kind: AuthScopeFailureType.AuthScopeFunction; | ||
} | ||
export interface UnknownAuthFailure { | ||
kind: AuthScopeFailureType.Unknown; | ||
} | ||
export interface AnyAuthScopesFailure { | ||
kind: AuthScopeFailureType.AnyAuthScopes; | ||
failures: AuthFailure[]; | ||
} | ||
export interface AllAuthScopesFailure { | ||
kind: AuthScopeFailureType.AllAuthScopes; | ||
failures: AuthFailure[]; | ||
} | ||
export interface GrantedScopeFailure { | ||
kind: AuthScopeFailureType.GrantedScope; | ||
scope: string; | ||
} | ||
export declare type AuthFailure = AuthScopeFailure | AuthScopeFunctionFailure | GrantedScopeFailure | AnyAuthScopesFailure | AllAuthScopesFailure | UnknownAuthFailure; | ||
export interface ForbiddenResult { | ||
message: string; | ||
failure: AuthFailure; | ||
} | ||
export interface ResolveStep<Types extends SchemaTypes> { | ||
run: (state: ResolveState<Types>, parent: unknown, args: Record<string, unknown>, context: {}, info: GraphQLResolveInfo) => MaybePromise<boolean>; | ||
run: (state: ResolveState<Types>, parent: unknown, args: Record<string, unknown>, context: {}, info: GraphQLResolveInfo) => MaybePromise<null | AuthFailure>; | ||
errorMessage: string | ((parent: unknown, args: Record<string, unknown>, context: {}, info: GraphQLResolveInfo) => string); | ||
} | ||
export declare type ContextForAuth<Types extends SchemaTypes, Scopes extends {}> = keyof Scopes & keyof Types['AuthContexts'] extends string ? Types['AuthContexts'][keyof Scopes & keyof Types['AuthContexts']] : Types['Context']; | ||
export declare type UnauthorizedResolver<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap> = (parent: ParentShape, args: InputShapeFromFields<Args>, context: Types['Context'], info: GraphQLResolveInfo, error: Error) => MaybePromise<ShapeFromTypeParam<Types, Type, Nullable>>; | ||
export declare type UnauthorizedErrorFn<Types extends SchemaTypes, ParentShape, Args extends InputFieldMap> = (parent: ParentShape, args: InputShapeFromFields<Args>, context: Types['Context'], info: GraphQLResolveInfo, result: ForbiddenResult) => Error | string; | ||
export interface UnauthorizedOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap> { | ||
unauthorizedError?: UnauthorizedErrorFn<Types, ParentShape, Args>; | ||
unauthorizedResolver?: UnauthorizedResolver<Types, ParentShape, Type, Nullable, Args>; | ||
} | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,2 +0,10 @@ | ||
export {}; | ||
export var AuthScopeFailureType; | ||
(function (AuthScopeFailureType) { | ||
AuthScopeFailureType["AuthScope"] = "AuthScope"; | ||
AuthScopeFailureType["AuthScopeFunction"] = "AuthScopeFunction"; | ||
AuthScopeFailureType["GrantedScope"] = "GrantedScope"; | ||
AuthScopeFailureType["AnyAuthScopes"] = "AnyAuthScopes"; | ||
AuthScopeFailureType["AllAuthScopes"] = "AllAuthScopes"; | ||
AuthScopeFailureType["Unknown"] = "Unknown"; | ||
})(AuthScopeFailureType || (AuthScopeFailureType = {})); | ||
//# sourceMappingURL=types.js.map |
@@ -0,5 +1,7 @@ | ||
import { AuthFailure } from '.'; | ||
export declare class ForbiddenError extends Error { | ||
code: string; | ||
constructor(message: string); | ||
result: AuthFailure; | ||
constructor(message: string, result?: AuthFailure); | ||
} | ||
//# sourceMappingURL=errors.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ForbiddenError = void 0; | ||
const _1 = require("."); | ||
class ForbiddenError extends Error { | ||
constructor(message) { | ||
constructor(message, result) { | ||
super(message); | ||
this.code = 'FORBIDDEN'; | ||
this.name = 'ForbiddenError'; | ||
this.result = result !== null && result !== void 0 ? result : { kind: _1.AuthScopeFailureType.Unknown }; | ||
Object.defineProperty(this, 'name', { value: 'ForbiddenError' }); | ||
@@ -10,0 +12,0 @@ } |
@@ -1,6 +0,4 @@ | ||
import { GraphQLResolveInfo } from 'graphql'; | ||
import { FieldKind, FieldNullability, FieldOptionsFromKind, FieldRef, InputFieldMap, InputShapeFromFields, MaybePromise, Normalize, Resolver, RootName, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; | ||
import { ForbiddenError } from './errors'; | ||
import { FieldKind, FieldNullability, FieldOptionsFromKind, FieldRef, InputFieldMap, InputShapeFromFields, Normalize, Resolver, RootName, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; | ||
import { FieldAuthScopes, FieldGrantScopes, ScopeAuthInitializer, ScopeAuthPluginOptions, TypeAuthScopes, TypeGrantScopes } from './types'; | ||
import { ContextForAuth, PothosScopeAuthPlugin } from '.'; | ||
import { ContextForAuth, PothosScopeAuthPlugin, UnauthorizedOptions } from '.'; | ||
declare global { | ||
@@ -12,3 +10,3 @@ export namespace PothosSchemaTypes { | ||
interface SchemaBuilderOptions<Types extends SchemaTypes> { | ||
scopeAuthOptions?: ScopeAuthPluginOptions; | ||
scopeAuthOptions?: ScopeAuthPluginOptions<Types>; | ||
authScopes: ScopeAuthInitializer<Types>; | ||
@@ -39,7 +37,6 @@ } | ||
} | ||
interface FieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, ResolveShape, ResolveReturnShape> { | ||
interface FieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, ResolveShape, ResolveReturnShape> extends UnauthorizedOptions<Types, ParentShape, Type, Nullable, Args> { | ||
authScopes?: FieldAuthScopes<Types, ParentShape, InputShapeFromFields<Args>>; | ||
grantScopes?: FieldGrantScopes<Types, ParentShape, InputShapeFromFields<Args>>; | ||
skipTypeScopes?: boolean; | ||
unauthorizedResolver?: (parent: ParentShape, args: InputShapeFromFields<Args>, context: Types['Context'], info: GraphQLResolveInfo, error: ForbiddenError) => MaybePromise<ShapeFromTypeParam<Types, Type, Nullable>>; | ||
} | ||
@@ -46,0 +43,0 @@ interface ObjectFieldOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap, ResolveReturnShape> extends FieldOptions<Types, ParentShape, Type, Nullable, Args, ParentShape, ResolveReturnShape> { |
@@ -5,4 +5,4 @@ import './global-types'; | ||
import { ResolveStep } from './types'; | ||
export * from './errors'; | ||
export * from './types'; | ||
export * from './errors'; | ||
declare const pluginName: "scopeAuth"; | ||
@@ -9,0 +9,0 @@ export default pluginName; |
@@ -30,4 +30,4 @@ "use strict"; | ||
const steps_1 = require("./steps"); | ||
__exportStar(require("./errors"), exports); | ||
__exportStar(require("./types"), exports); | ||
__exportStar(require("./errors"), exports); | ||
const pluginName = 'scopeAuth'; | ||
@@ -34,0 +34,0 @@ exports.default = pluginName; |
import { GraphQLResolveInfo } from 'graphql'; | ||
import { MaybePromise, Path, SchemaTypes } from '@pothos/core'; | ||
import { ScopeLoaderMap } from './types'; | ||
import { PothosScopeAuthPlugin } from '.'; | ||
import { AuthFailure, PothosScopeAuthPlugin } from '.'; | ||
export default class RequestCache<Types extends SchemaTypes> { | ||
plugin: PothosScopeAuthPlugin<Types>; | ||
context: Types["Context"]; | ||
mapCache: Map<{}, MaybePromise<boolean>>; | ||
scopeCache: Map<keyof Types["AuthScopes"], Map<unknown, MaybePromise<boolean>>>; | ||
typeCache: Map<string, Map<unknown, MaybePromise<boolean>>>; | ||
typeGrants: Map<string, Map<unknown, MaybePromise<boolean>>>; | ||
mapCache: Map<{}, MaybePromise<AuthFailure | null>>; | ||
scopeCache: Map<keyof Types["AuthScopes"], Map<unknown, MaybePromise<AuthFailure | null>>>; | ||
typeCache: Map<string, Map<unknown, MaybePromise<AuthFailure | null>>>; | ||
typeGrants: Map<string, Map<unknown, MaybePromise<null>>>; | ||
grantCache: Map<string, Set<string>>; | ||
@@ -18,7 +18,7 @@ scopes?: MaybePromise<ScopeLoaderMap<Types>>; | ||
withScopes<T>(cb: (scopes: ScopeLoaderMap<Types>) => MaybePromise<T>): MaybePromise<T>; | ||
saveGrantedScopes(scopes: string[], path: Path | undefined): boolean; | ||
saveGrantedScopes(scopes: string[], path: Path | undefined): null; | ||
testGrantedScopes(scope: string, path: Path): boolean; | ||
grantTypeScopes(type: string, parent: unknown, info: GraphQLResolveInfo, cb: () => MaybePromise<string[]>): MaybePromise<boolean>; | ||
evaluateScopeLoader<T extends keyof Types['AuthScopes']>(scopes: ScopeLoaderMap<Types>, name: T, arg: Types['AuthScopes'][T]): MaybePromise<boolean>; | ||
grantTypeScopes(type: string, parent: unknown, info: GraphQLResolveInfo, cb: () => MaybePromise<string[]>): Promise<null>; | ||
evaluateScopeLoader<T extends keyof Types['AuthScopes']>(scopes: ScopeLoaderMap<Types>, name: T, arg: Types['AuthScopes'][T]): import("./types").AuthScopeFailure | import("./types").AuthScopeFunctionFailure | import("./types").GrantedScopeFailure | import("./types").AnyAuthScopesFailure | import("./types").AllAuthScopesFailure | import("./types").UnknownAuthFailure | Promise<AuthFailure | null>; | ||
} | ||
//# sourceMappingURL=request-cache.d.ts.map |
@@ -5,2 +5,3 @@ "use strict"; | ||
const util_1 = require("./util"); | ||
const _1 = require("."); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
@@ -53,3 +54,3 @@ const requestCache = new WeakMap(); | ||
} | ||
return true; | ||
return null; | ||
} | ||
@@ -93,3 +94,21 @@ testGrantedScopes(scope, path) { | ||
} | ||
cache.set(arg, loader(arg)); | ||
const result = loader(arg); | ||
if ((0, core_1.isThenable)(result)) { | ||
cache.set(arg, result.then((r) => r | ||
? null | ||
: { | ||
kind: _1.AuthScopeFailureType.AuthScope, | ||
scope: name, | ||
parameter: arg, | ||
})); | ||
} | ||
else { | ||
cache.set(arg, result | ||
? null | ||
: { | ||
kind: _1.AuthScopeFailureType.AuthScope, | ||
scope: name, | ||
parameter: arg, | ||
}); | ||
} | ||
} | ||
@@ -96,0 +115,0 @@ return cache.get(arg); |
@@ -11,8 +11,9 @@ "use strict"; | ||
const resolve_state_1 = __importDefault(require("./resolve-state")); | ||
const defaultUnauthorizedResolver = (root, args, context, info, error) => { | ||
const defaultUnauthorizedResolver = (_root, _args, _context, _info, error) => { | ||
throw error; | ||
}; | ||
function resolveHelper(steps, plugin, fieldConfig) { | ||
var _a; | ||
var _a, _b, _c, _d; | ||
const unauthorizedResolver = (_a = fieldConfig.pothosOptions.unauthorizedResolver) !== null && _a !== void 0 ? _a : defaultUnauthorizedResolver; | ||
const createError = (_d = (_b = fieldConfig.pothosOptions.unauthorizedError) !== null && _b !== void 0 ? _b : (_c = plugin.builder.options.scopeAuthOptions) === null || _c === void 0 ? void 0 : _c.unauthorizedError) !== null && _d !== void 0 ? _d : ((parent, args, context, info, result) => result.message); | ||
return (parent, args, context, info) => { | ||
@@ -26,6 +27,10 @@ const state = new resolve_state_1.default(request_cache_1.default.fromContext(context, plugin)); | ||
return stepResult.then((result) => { | ||
if (!result) { | ||
return unauthorizedResolver(parent, args, context, info, new errors_1.ForbiddenError(typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage)); | ||
if (result) { | ||
const error = createError(parent, args, context, info, { | ||
message: typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
failure: result, | ||
}); | ||
return unauthorizedResolver(parent, args, context, info, typeof error === 'string' ? new errors_1.ForbiddenError(error, result) : error); | ||
} | ||
@@ -35,6 +40,10 @@ return runSteps(i + 1); | ||
} | ||
if (!stepResult) { | ||
return unauthorizedResolver(parent, args, context, info, new errors_1.ForbiddenError(typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage)); | ||
if (stepResult) { | ||
const error = createError(parent, args, context, info, { | ||
message: typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
failure: stepResult, | ||
}); | ||
return unauthorizedResolver(parent, args, context, info, typeof error === 'string' ? new errors_1.ForbiddenError(error, stepResult) : error); | ||
} | ||
@@ -41,0 +50,0 @@ } |
@@ -5,2 +5,3 @@ import { GraphQLResolveInfo } from 'graphql'; | ||
import { AuthScopeMap, ScopeLoaderMap, TypeAuthScopesFunction } from './types'; | ||
import { AuthFailure } from '.'; | ||
export default class ResolveState<Types extends SchemaTypes> { | ||
@@ -10,6 +11,6 @@ cache: RequestCache<Types>; | ||
constructor(cache: RequestCache<Types>); | ||
evaluateScopeMapWithScopes({ $all, $any, $granted, ...map }: AuthScopeMap<Types>, scopes: ScopeLoaderMap<Types>, info: GraphQLResolveInfo, forAll: boolean): MaybePromise<boolean>; | ||
evaluateScopeMap(map: AuthScopeMap<Types>, info: GraphQLResolveInfo, forAll?: boolean): MaybePromise<boolean>; | ||
evaluateTypeScopeFunction(authScopes: TypeAuthScopesFunction<Types, unknown>, type: string, parent: unknown, info: GraphQLResolveInfo): MaybePromise<boolean>; | ||
evaluateScopeMapWithScopes({ $all, $any, $granted, ...map }: AuthScopeMap<Types>, scopes: ScopeLoaderMap<Types>, info: GraphQLResolveInfo, forAll: boolean): MaybePromise<null | AuthFailure>; | ||
evaluateScopeMap(map: AuthScopeMap<Types> | boolean, info: GraphQLResolveInfo, forAll?: boolean): MaybePromise<null | AuthFailure>; | ||
evaluateTypeScopeFunction(authScopes: TypeAuthScopesFunction<Types, unknown>, type: string, parent: unknown, info: GraphQLResolveInfo): import("./types").AuthScopeFailure | import("./types").AuthScopeFunctionFailure | import("./types").GrantedScopeFailure | import("./types").AnyAuthScopesFailure | import("./types").AllAuthScopesFailure | import("./types").UnknownAuthFailure | Promise<AuthFailure | null>; | ||
} | ||
//# sourceMappingURL=resolve-state.d.ts.map |
@@ -5,2 +5,3 @@ "use strict"; | ||
const util_1 = require("./util"); | ||
const _1 = require("."); | ||
class ResolveState { | ||
@@ -12,7 +13,17 @@ constructor(cache) { | ||
const scopeNames = Object.keys(map); | ||
const problems = []; | ||
const failure = { | ||
kind: forAll ? _1.AuthScopeFailureType.AllAuthScopes : _1.AuthScopeFailureType.AnyAuthScopes, | ||
failures: problems, | ||
}; | ||
const loaderList = []; | ||
for (const scopeName of scopeNames) { | ||
if (scopes[scopeName] == null || scopes[scopeName] === false) { | ||
problems.push({ | ||
kind: _1.AuthScopeFailureType.AuthScope, | ||
scope: scopeName, | ||
parameter: map[scopeName], | ||
}); | ||
if (forAll) { | ||
return false; | ||
return failure; | ||
} | ||
@@ -26,5 +37,15 @@ // eslint-disable-next-line no-continue | ||
} | ||
else if (scope === !forAll) { | ||
return scope; | ||
else if (scope && !forAll) { | ||
return null; | ||
} | ||
else if (!scope) { | ||
problems.push({ | ||
kind: _1.AuthScopeFailureType.AuthScope, | ||
scope: scopeName, | ||
parameter: map[scopeName], | ||
}); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
} | ||
@@ -34,30 +55,44 @@ const promises = []; | ||
const result = this.cache.testGrantedScopes($granted, info.path); | ||
if (result !== forAll) { | ||
return result; | ||
if (result && !forAll) { | ||
return null; | ||
} | ||
if (!result) { | ||
problems.push({ | ||
kind: _1.AuthScopeFailureType.GrantedScope, | ||
scope: $granted, | ||
}); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
} | ||
if ($any) { | ||
const anyResult = this.evaluateScopeMap($any, info); | ||
if (typeof anyResult === 'boolean') { | ||
if (anyResult === !forAll) { | ||
return anyResult; | ||
if ((0, core_1.isThenable)(anyResult)) { | ||
promises.push(anyResult); | ||
} | ||
else if (anyResult === null && !forAll) { | ||
return null; | ||
} | ||
else if (anyResult) { | ||
problems.push(anyResult); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
else { | ||
promises.push(anyResult); | ||
} | ||
} | ||
if ($all) { | ||
const allResult = this.evaluateScopeMap($all, info, true); | ||
if (typeof allResult === 'boolean') { | ||
if (allResult === !forAll) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => allResult); | ||
} | ||
return allResult; | ||
if ((0, core_1.isThenable)(allResult)) { | ||
promises.push(allResult); | ||
} | ||
else if (allResult === null && !forAll) { | ||
return resolveAndReturn(null); | ||
} | ||
else if (allResult) { | ||
problems.push(allResult); | ||
if (forAll) { | ||
return resolveAndReturn(failure); | ||
} | ||
} | ||
else { | ||
promises.push(allResult); | ||
} | ||
} | ||
@@ -69,15 +104,45 @@ for (const [loaderName, arg] of loaderList) { | ||
} | ||
else if (result === !forAll) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => result); | ||
else if (result === null && !forAll) { | ||
return resolveAndReturn(null); | ||
} | ||
else if (result) { | ||
problems.push(result); | ||
if (forAll) { | ||
return resolveAndReturn(failure); | ||
} | ||
return result; | ||
} | ||
} | ||
if (promises.length === 0) { | ||
return forAll; | ||
return forAll && problems.length === 0 ? null : failure; | ||
} | ||
return Promise.all(promises).then((results) => forAll ? results.every(Boolean) : results.some(Boolean)); | ||
return Promise.all(promises).then((results) => { | ||
let hasSuccess = false; | ||
results.forEach((result) => { | ||
if (result) { | ||
problems.push(result); | ||
} | ||
else { | ||
hasSuccess = true; | ||
} | ||
}); | ||
if (forAll) { | ||
return problems.length > 0 ? failure : null; | ||
} | ||
return hasSuccess ? null : failure; | ||
}); | ||
function resolveAndReturn(val) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => val); | ||
} | ||
return val; | ||
} | ||
} | ||
evaluateScopeMap(map, info, forAll = false) { | ||
if (typeof map === 'boolean') { | ||
return map | ||
? null | ||
: { | ||
kind: _1.AuthScopeFailureType.AuthScopeFunction, | ||
}; | ||
} | ||
if (!this.cache.mapCache.has(map)) { | ||
@@ -101,6 +166,6 @@ const result = this.cache.withScopes((scopes) => this.evaluateScopeMapWithScopes(map, scopes, info, forAll)); | ||
if ((0, core_1.isThenable)(result)) { | ||
cache.set(parent, result.then((resolved) => typeof resolved === 'boolean' ? resolved : this.evaluateScopeMap(resolved, info))); | ||
cache.set(parent, result.then((resolved) => this.evaluateScopeMap(resolved, info))); | ||
} | ||
else { | ||
cache.set(parent, typeof result === 'boolean' ? result : this.evaluateScopeMap(result, info)); | ||
cache.set(parent, this.evaluateScopeMap(result, info)); | ||
} | ||
@@ -107,0 +172,0 @@ } |
@@ -33,12 +33,4 @@ "use strict"; | ||
if ((0, core_1.isThenable)(scopeMap)) { | ||
return scopeMap.then((resolved) => { | ||
if (typeof resolved === 'boolean') { | ||
return resolved; | ||
} | ||
return state.evaluateScopeMap(resolved, info); | ||
}); | ||
return scopeMap.then((resolved) => state.evaluateScopeMap(resolved, info)); | ||
} | ||
if (typeof scopeMap === 'boolean') { | ||
return scopeMap; | ||
} | ||
return state.evaluateScopeMap(scopeMap, info); | ||
@@ -60,3 +52,3 @@ }, | ||
state.cache.saveGrantedScopes(grantScopes, info.path); | ||
return true; | ||
return null; | ||
} | ||
@@ -67,7 +59,7 @@ const result = grantScopes(parent, args, context, info); | ||
state.cache.saveGrantedScopes(resolved, info.path); | ||
return true; | ||
return null; | ||
}); | ||
} | ||
state.cache.saveGrantedScopes(result, info.path); | ||
return true; | ||
return null; | ||
}, | ||
@@ -85,7 +77,7 @@ }; | ||
state.resolveValue = resolved; | ||
return true; | ||
return null; | ||
}); | ||
} | ||
state.resolveValue = result; | ||
return true; | ||
return null; | ||
}, | ||
@@ -92,0 +84,0 @@ }; |
import { GraphQLResolveInfo } from 'graphql'; | ||
import { MaybePromise, Merge, SchemaTypes } from '@pothos/core'; | ||
import { FieldNullability, InputFieldMap, InputShapeFromFields, MaybePromise, Merge, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; | ||
import ResolveState from './resolve-state'; | ||
export interface ScopeAuthPluginOptions { | ||
export interface ScopeAuthPluginOptions<Types extends SchemaTypes> { | ||
unauthorizedError?: UnauthorizedErrorFn<Types, unknown, {}>; | ||
} | ||
@@ -21,7 +22,49 @@ export interface BuiltInScopes<Types extends SchemaTypes> { | ||
export declare type FieldGrantScopes<Types extends SchemaTypes, Parent, Args extends {}> = string[] | ((parent: Parent, args: Args, context: Types['Context'], info: GraphQLResolveInfo) => MaybePromise<string[]>); | ||
export declare enum AuthScopeFailureType { | ||
AuthScope = "AuthScope", | ||
AuthScopeFunction = "AuthScopeFunction", | ||
GrantedScope = "GrantedScope", | ||
AnyAuthScopes = "AnyAuthScopes", | ||
AllAuthScopes = "AllAuthScopes", | ||
Unknown = "Unknown" | ||
} | ||
export interface AuthScopeFailure { | ||
kind: AuthScopeFailureType.AuthScope; | ||
scope: string; | ||
parameter: unknown; | ||
} | ||
export interface AuthScopeFunctionFailure { | ||
kind: AuthScopeFailureType.AuthScopeFunction; | ||
} | ||
export interface UnknownAuthFailure { | ||
kind: AuthScopeFailureType.Unknown; | ||
} | ||
export interface AnyAuthScopesFailure { | ||
kind: AuthScopeFailureType.AnyAuthScopes; | ||
failures: AuthFailure[]; | ||
} | ||
export interface AllAuthScopesFailure { | ||
kind: AuthScopeFailureType.AllAuthScopes; | ||
failures: AuthFailure[]; | ||
} | ||
export interface GrantedScopeFailure { | ||
kind: AuthScopeFailureType.GrantedScope; | ||
scope: string; | ||
} | ||
export declare type AuthFailure = AuthScopeFailure | AuthScopeFunctionFailure | GrantedScopeFailure | AnyAuthScopesFailure | AllAuthScopesFailure | UnknownAuthFailure; | ||
export interface ForbiddenResult { | ||
message: string; | ||
failure: AuthFailure; | ||
} | ||
export interface ResolveStep<Types extends SchemaTypes> { | ||
run: (state: ResolveState<Types>, parent: unknown, args: Record<string, unknown>, context: {}, info: GraphQLResolveInfo) => MaybePromise<boolean>; | ||
run: (state: ResolveState<Types>, parent: unknown, args: Record<string, unknown>, context: {}, info: GraphQLResolveInfo) => MaybePromise<null | AuthFailure>; | ||
errorMessage: string | ((parent: unknown, args: Record<string, unknown>, context: {}, info: GraphQLResolveInfo) => string); | ||
} | ||
export declare type ContextForAuth<Types extends SchemaTypes, Scopes extends {}> = keyof Scopes & keyof Types['AuthContexts'] extends string ? Types['AuthContexts'][keyof Scopes & keyof Types['AuthContexts']] : Types['Context']; | ||
export declare type UnauthorizedResolver<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap> = (parent: ParentShape, args: InputShapeFromFields<Args>, context: Types['Context'], info: GraphQLResolveInfo, error: Error) => MaybePromise<ShapeFromTypeParam<Types, Type, Nullable>>; | ||
export declare type UnauthorizedErrorFn<Types extends SchemaTypes, ParentShape, Args extends InputFieldMap> = (parent: ParentShape, args: InputShapeFromFields<Args>, context: Types['Context'], info: GraphQLResolveInfo, result: ForbiddenResult) => Error | string; | ||
export interface UnauthorizedOptions<Types extends SchemaTypes, ParentShape, Type extends TypeParam<Types>, Nullable extends FieldNullability<Type>, Args extends InputFieldMap> { | ||
unauthorizedError?: UnauthorizedErrorFn<Types, ParentShape, Args>; | ||
unauthorizedResolver?: UnauthorizedResolver<Types, ParentShape, Type, Nullable, Args>; | ||
} | ||
//# sourceMappingURL=types.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.AuthScopeFailureType = void 0; | ||
var AuthScopeFailureType; | ||
(function (AuthScopeFailureType) { | ||
AuthScopeFailureType["AuthScope"] = "AuthScope"; | ||
AuthScopeFailureType["AuthScopeFunction"] = "AuthScopeFunction"; | ||
AuthScopeFailureType["GrantedScope"] = "GrantedScope"; | ||
AuthScopeFailureType["AnyAuthScopes"] = "AnyAuthScopes"; | ||
AuthScopeFailureType["AllAuthScopes"] = "AllAuthScopes"; | ||
AuthScopeFailureType["Unknown"] = "Unknown"; | ||
})(AuthScopeFailureType = exports.AuthScopeFailureType || (exports.AuthScopeFailureType = {})); | ||
//# sourceMappingURL=types.js.map |
{ | ||
"name": "@pothos/plugin-scope-auth", | ||
"version": "3.0.2", | ||
"version": "3.1.0", | ||
"description": "A Pothos plugin for adding scope based authorization checks to your GraphQL Schema", | ||
@@ -39,3 +39,3 @@ "main": "./lib/index.js", | ||
"@pothos/core": "3.1.2", | ||
"@pothos/test-utils": "1.0.0", | ||
"@pothos/test-utils": "1.0.1", | ||
"graphql": "16.3.0", | ||
@@ -42,0 +42,0 @@ "graphql-tag": "^2.12.6" |
@@ -0,8 +1,12 @@ | ||
import { AuthFailure, AuthScopeFailureType } from '.'; | ||
export class ForbiddenError extends Error { | ||
code = 'FORBIDDEN'; | ||
result: AuthFailure; | ||
constructor(message: string) { | ||
constructor(message: string, result?: AuthFailure) { | ||
super(message); | ||
this.name = 'ForbiddenError'; | ||
this.result = result ?? { kind: AuthScopeFailureType.Unknown }; | ||
@@ -9,0 +13,0 @@ Object.defineProperty(this, 'name', { value: 'ForbiddenError' }); |
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
import { GraphQLResolveInfo } from 'graphql'; | ||
import { | ||
@@ -10,4 +9,2 @@ FieldKind, | ||
InputShapeFromFields, | ||
ListResolveValue, | ||
MaybePromise, | ||
Normalize, | ||
@@ -20,3 +17,2 @@ Resolver, | ||
} from '@pothos/core'; | ||
import { ForbiddenError } from './errors'; | ||
import { | ||
@@ -30,3 +26,3 @@ FieldAuthScopes, | ||
} from './types'; | ||
import { ContextForAuth, PothosScopeAuthPlugin } from '.'; | ||
import { ContextForAuth, PothosScopeAuthPlugin, UnauthorizedOptions } from '.'; | ||
@@ -40,3 +36,3 @@ declare global { | ||
export interface SchemaBuilderOptions<Types extends SchemaTypes> { | ||
scopeAuthOptions?: ScopeAuthPluginOptions; | ||
scopeAuthOptions?: ScopeAuthPluginOptions<Types>; | ||
authScopes: ScopeAuthInitializer<Types>; | ||
@@ -86,13 +82,6 @@ } | ||
ResolveReturnShape, | ||
> { | ||
> extends UnauthorizedOptions<Types, ParentShape, Type, Nullable, Args> { | ||
authScopes?: FieldAuthScopes<Types, ParentShape, InputShapeFromFields<Args>>; | ||
grantScopes?: FieldGrantScopes<Types, ParentShape, InputShapeFromFields<Args>>; | ||
skipTypeScopes?: boolean; | ||
unauthorizedResolver?: ( | ||
parent: ParentShape, | ||
args: InputShapeFromFields<Args>, | ||
context: Types['Context'], | ||
info: GraphQLResolveInfo, | ||
error: ForbiddenError, | ||
) => MaybePromise<ShapeFromTypeParam<Types, Type, Nullable>>; | ||
} | ||
@@ -99,0 +88,0 @@ |
@@ -0,1 +1,2 @@ | ||
/* eslint-disable @typescript-eslint/promise-function-async */ | ||
import { GraphQLResolveInfo } from 'graphql'; | ||
@@ -5,3 +6,3 @@ import { isThenable, MaybePromise, Path, SchemaTypes } from '@pothos/core'; | ||
import { cacheKey } from './util'; | ||
import { PothosScopeAuthPlugin } from '.'; | ||
import { AuthFailure, AuthScopeFailureType, PothosScopeAuthPlugin } from '.'; | ||
@@ -16,9 +17,9 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
mapCache = new Map<{}, MaybePromise<boolean>>(); | ||
mapCache = new Map<{}, MaybePromise<null | AuthFailure>>(); | ||
scopeCache = new Map<keyof Types['AuthScopes'], Map<unknown, MaybePromise<boolean>>>(); | ||
scopeCache = new Map<keyof Types['AuthScopes'], Map<unknown, MaybePromise<AuthFailure | null>>>(); | ||
typeCache = new Map<string, Map<unknown, MaybePromise<boolean>>>(); | ||
typeCache = new Map<string, Map<unknown, MaybePromise<null | AuthFailure>>>(); | ||
typeGrants = new Map<string, Map<unknown, MaybePromise<boolean>>>(); | ||
typeGrants = new Map<string, Map<unknown, MaybePromise<null>>>(); | ||
@@ -83,3 +84,3 @@ grantCache = new Map<string, Set<string>>(); | ||
return true; | ||
return null; | ||
} | ||
@@ -108,3 +109,3 @@ | ||
if (!this.typeGrants.has(type)) { | ||
this.typeGrants.set(type, new Map<string, Promise<boolean>>()); | ||
this.typeGrants.set(type, new Map<string, Promise<null>>()); | ||
} | ||
@@ -136,3 +137,3 @@ | ||
if (!this.scopeCache.has(name)) { | ||
this.scopeCache.set(name, new Map<string, Promise<boolean>>()); | ||
this.scopeCache.set(name, new Map()); | ||
} | ||
@@ -151,3 +152,29 @@ | ||
cache.set(arg, (loader as (param: Types['AuthScopes'][T]) => MaybePromise<boolean>)(arg)); | ||
const result = (loader as (param: Types['AuthScopes'][T]) => MaybePromise<boolean>)(arg); | ||
if (isThenable(result)) { | ||
cache.set( | ||
arg, | ||
result.then((r) => | ||
r | ||
? null | ||
: { | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: name as string, | ||
parameter: arg, | ||
}, | ||
), | ||
); | ||
} else { | ||
cache.set( | ||
arg, | ||
result | ||
? null | ||
: { | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: name as string, | ||
parameter: arg, | ||
}, | ||
); | ||
} | ||
} | ||
@@ -154,0 +181,0 @@ |
@@ -6,11 +6,11 @@ import { GraphQLResolveInfo } from 'graphql'; | ||
import ResolveState from './resolve-state'; | ||
import { ResolveStep } from './types'; | ||
import { PothosScopeAuthPlugin } from '.'; | ||
import { ResolveStep, UnauthorizedResolver } from './types'; | ||
import { PothosScopeAuthPlugin, UnauthorizedErrorFn } from '.'; | ||
const defaultUnauthorizedResolver = ( | ||
root: unknown, | ||
args: unknown, | ||
context: unknown, | ||
info: GraphQLResolveInfo, | ||
error: ForbiddenError, | ||
const defaultUnauthorizedResolver: UnauthorizedResolver<never, never, never, never, never> = ( | ||
_root, | ||
_args, | ||
_context, | ||
_info, | ||
error, | ||
) => { | ||
@@ -28,2 +28,7 @@ throw error; | ||
const createError: UnauthorizedErrorFn<Types, object, {}> = | ||
fieldConfig.pothosOptions.unauthorizedError ?? | ||
plugin.builder.options.scopeAuthOptions?.unauthorizedError ?? | ||
((parent, args, context, info, result) => result.message); | ||
return (parent: unknown, args: {}, context: Types['Context'], info: GraphQLResolveInfo) => { | ||
@@ -40,13 +45,17 @@ const state = new ResolveState(RequestCache.fromContext(context, plugin)); | ||
return stepResult.then((result) => { | ||
if (!result) { | ||
if (result) { | ||
const error = createError(parent as object, args, context, info, { | ||
message: | ||
typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
failure: result, | ||
}); | ||
return unauthorizedResolver( | ||
parent as never, | ||
args, | ||
context, | ||
context as never, | ||
info, | ||
new ForbiddenError( | ||
typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
), | ||
typeof error === 'string' ? new ForbiddenError(error, result) : error, | ||
); | ||
@@ -59,13 +68,17 @@ } | ||
if (!stepResult) { | ||
if (stepResult) { | ||
const error = createError(parent as object, args, context, info, { | ||
message: | ||
typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
failure: stepResult, | ||
}); | ||
return unauthorizedResolver( | ||
parent as never, | ||
args, | ||
context, | ||
context as never, | ||
info, | ||
new ForbiddenError( | ||
typeof errorMessage === 'function' | ||
? errorMessage(parent, args, context, info) | ||
: errorMessage, | ||
), | ||
typeof error === 'string' ? new ForbiddenError(error, stepResult) : error, | ||
); | ||
@@ -72,0 +85,0 @@ } |
@@ -6,2 +6,3 @@ import { GraphQLResolveInfo } from 'graphql'; | ||
import { canCache } from './util'; | ||
import { AuthFailure, AuthScopeFailureType } from '.'; | ||
@@ -22,4 +23,9 @@ export default class ResolveState<Types extends SchemaTypes> { | ||
forAll: boolean, | ||
): MaybePromise<boolean> { | ||
): MaybePromise<null | AuthFailure> { | ||
const scopeNames = Object.keys(map) as (keyof typeof map)[]; | ||
const problems: AuthFailure[] = []; | ||
const failure: AuthFailure = { | ||
kind: forAll ? AuthScopeFailureType.AllAuthScopes : AuthScopeFailureType.AnyAuthScopes, | ||
failures: problems, | ||
}; | ||
@@ -33,4 +39,9 @@ const loaderList: [ | ||
if (scopes[scopeName] == null || scopes[scopeName] === false) { | ||
problems.push({ | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: scopeName as string, | ||
parameter: map[scopeName], | ||
}); | ||
if (forAll) { | ||
return false; | ||
return failure; | ||
} | ||
@@ -48,8 +59,18 @@ // eslint-disable-next-line no-continue | ||
loaderList.push([scopeName, map[scopeName]]); | ||
} else if (scope === !forAll) { | ||
return scope; | ||
} else if (scope && !forAll) { | ||
return null; | ||
} else if (!scope) { | ||
problems.push({ | ||
kind: AuthScopeFailureType.AuthScope, | ||
scope: scopeName as string, | ||
parameter: map[scopeName], | ||
}); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
} | ||
const promises: Promise<boolean>[] = []; | ||
const promises: Promise<null | AuthFailure>[] = []; | ||
@@ -59,5 +80,16 @@ if ($granted) { | ||
if (result !== forAll) { | ||
return result; | ||
if (result && !forAll) { | ||
return null; | ||
} | ||
if (!result) { | ||
problems.push({ | ||
kind: AuthScopeFailureType.GrantedScope, | ||
scope: $granted, | ||
}); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} | ||
} | ||
@@ -68,8 +100,12 @@ | ||
if (typeof anyResult === 'boolean') { | ||
if (anyResult === !forAll) { | ||
return anyResult; | ||
if (isThenable(anyResult)) { | ||
promises.push(anyResult); | ||
} else if (anyResult === null && !forAll) { | ||
return null; | ||
} else if (anyResult) { | ||
problems.push(anyResult); | ||
if (forAll) { | ||
return failure; | ||
} | ||
} else { | ||
promises.push(anyResult); | ||
} | ||
@@ -81,12 +117,12 @@ } | ||
if (typeof allResult === 'boolean') { | ||
if (allResult === !forAll) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => allResult); | ||
} | ||
if (isThenable(allResult)) { | ||
promises.push(allResult); | ||
} else if (allResult === null && !forAll) { | ||
return resolveAndReturn(null); | ||
} else if (allResult) { | ||
problems.push(allResult); | ||
return allResult; | ||
if (forAll) { | ||
return resolveAndReturn(failure); | ||
} | ||
} else { | ||
promises.push(allResult); | ||
} | ||
@@ -100,8 +136,10 @@ } | ||
promises.push(result); | ||
} else if (result === !forAll) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => result); | ||
} else if (result === null && !forAll) { | ||
return resolveAndReturn(null); | ||
} else if (result) { | ||
problems.push(result); | ||
if (forAll) { | ||
return resolveAndReturn(failure); | ||
} | ||
return result; | ||
} | ||
@@ -111,15 +149,44 @@ } | ||
if (promises.length === 0) { | ||
return forAll; | ||
return forAll && problems.length === 0 ? null : failure; | ||
} | ||
return Promise.all(promises).then((results) => | ||
forAll ? results.every(Boolean) : results.some(Boolean), | ||
); | ||
return Promise.all(promises).then((results) => { | ||
let hasSuccess = false; | ||
results.forEach((result) => { | ||
if (result) { | ||
problems.push(result); | ||
} else { | ||
hasSuccess = true; | ||
} | ||
}); | ||
if (forAll) { | ||
return problems.length > 0 ? failure : null; | ||
} | ||
return hasSuccess ? null : failure; | ||
}); | ||
function resolveAndReturn(val: null | AuthFailure) { | ||
if (promises.length > 0) { | ||
return Promise.all(promises).then(() => val); | ||
} | ||
return val; | ||
} | ||
} | ||
evaluateScopeMap( | ||
map: AuthScopeMap<Types>, | ||
map: AuthScopeMap<Types> | boolean, | ||
info: GraphQLResolveInfo, | ||
forAll = false, | ||
): MaybePromise<boolean> { | ||
): MaybePromise<null | AuthFailure> { | ||
if (typeof map === 'boolean') { | ||
return map | ||
? null | ||
: { | ||
kind: AuthScopeFailureType.AuthScopeFunction, | ||
}; | ||
} | ||
if (!this.cache.mapCache.has(map)) { | ||
@@ -149,3 +216,3 @@ const result = this.cache.withScopes((scopes) => | ||
if (!typeCache.has(type)) { | ||
typeCache.set(type, new Map<unknown, MaybePromise<boolean>>()); | ||
typeCache.set(type, new Map()); | ||
} | ||
@@ -161,11 +228,6 @@ | ||
parent, | ||
result.then((resolved) => | ||
typeof resolved === 'boolean' ? resolved : this.evaluateScopeMap(resolved, info), | ||
), | ||
result.then((resolved) => this.evaluateScopeMap(resolved, info)), | ||
); | ||
} else { | ||
cache.set( | ||
parent, | ||
typeof result === 'boolean' ? result : this.evaluateScopeMap(result, info), | ||
); | ||
cache.set(parent, this.evaluateScopeMap(result, info)); | ||
} | ||
@@ -172,0 +234,0 @@ } |
@@ -54,15 +54,5 @@ /* eslint-disable no-param-reassign */ | ||
if (isThenable(scopeMap)) { | ||
return scopeMap.then((resolved) => { | ||
if (typeof resolved === 'boolean') { | ||
return resolved; | ||
} | ||
return state.evaluateScopeMap(resolved, info); | ||
}); | ||
return scopeMap.then((resolved) => state.evaluateScopeMap(resolved, info)); | ||
} | ||
if (typeof scopeMap === 'boolean') { | ||
return scopeMap; | ||
} | ||
return state.evaluateScopeMap(scopeMap, info); | ||
@@ -90,3 +80,3 @@ }, | ||
return true; | ||
return null; | ||
} | ||
@@ -99,3 +89,3 @@ const result = grantScopes(parent as {}, args, context, info); | ||
return true; | ||
return null; | ||
}); | ||
@@ -106,3 +96,3 @@ } | ||
return true; | ||
return null; | ||
}, | ||
@@ -125,3 +115,3 @@ }; | ||
return true; | ||
return null; | ||
}); | ||
@@ -132,5 +122,5 @@ } | ||
return true; | ||
return null; | ||
}, | ||
}; | ||
} |
105
src/types.ts
import { GraphQLResolveInfo } from 'graphql'; | ||
import { MaybePromise, Merge, SchemaTypes } from '@pothos/core'; | ||
import { | ||
FieldNullability, | ||
InputFieldMap, | ||
InputShapeFromFields, | ||
MaybePromise, | ||
Merge, | ||
SchemaTypes, | ||
ShapeFromTypeParam, | ||
TypeParam, | ||
} from '@pothos/core'; | ||
import ResolveState from './resolve-state'; | ||
// eslint-disable-next-line @typescript-eslint/no-empty-interface | ||
export interface ScopeAuthPluginOptions {} | ||
export interface ScopeAuthPluginOptions<Types extends SchemaTypes> { | ||
unauthorizedError?: UnauthorizedErrorFn<Types, unknown, {}>; | ||
} | ||
@@ -60,2 +70,52 @@ export interface BuiltInScopes<Types extends SchemaTypes> { | ||
export enum AuthScopeFailureType { | ||
AuthScope = 'AuthScope', | ||
AuthScopeFunction = 'AuthScopeFunction', | ||
GrantedScope = 'GrantedScope', | ||
AnyAuthScopes = 'AnyAuthScopes', | ||
AllAuthScopes = 'AllAuthScopes', | ||
Unknown = 'Unknown', | ||
} | ||
export interface AuthScopeFailure { | ||
kind: AuthScopeFailureType.AuthScope; | ||
scope: string; | ||
parameter: unknown; | ||
} | ||
export interface AuthScopeFunctionFailure { | ||
kind: AuthScopeFailureType.AuthScopeFunction; | ||
} | ||
export interface UnknownAuthFailure { | ||
kind: AuthScopeFailureType.Unknown; | ||
} | ||
export interface AnyAuthScopesFailure { | ||
kind: AuthScopeFailureType.AnyAuthScopes; | ||
failures: AuthFailure[]; | ||
} | ||
export interface AllAuthScopesFailure { | ||
kind: AuthScopeFailureType.AllAuthScopes; | ||
failures: AuthFailure[]; | ||
} | ||
export interface GrantedScopeFailure { | ||
kind: AuthScopeFailureType.GrantedScope; | ||
scope: string; | ||
} | ||
export type AuthFailure = | ||
| AuthScopeFailure | ||
| AuthScopeFunctionFailure | ||
| GrantedScopeFailure | ||
| AnyAuthScopesFailure | ||
| AllAuthScopesFailure | ||
| UnknownAuthFailure; | ||
export interface ForbiddenResult { | ||
message: string; | ||
failure: AuthFailure; | ||
} | ||
export interface ResolveStep<Types extends SchemaTypes> { | ||
@@ -68,3 +128,3 @@ run: ( | ||
info: GraphQLResolveInfo, | ||
) => MaybePromise<boolean>; | ||
) => MaybePromise<null | AuthFailure>; | ||
errorMessage: | ||
@@ -84,1 +144,38 @@ | string | ||
: Types['Context']; | ||
export type UnauthorizedResolver< | ||
Types extends SchemaTypes, | ||
ParentShape, | ||
Type extends TypeParam<Types>, | ||
Nullable extends FieldNullability<Type>, | ||
Args extends InputFieldMap, | ||
> = ( | ||
parent: ParentShape, | ||
args: InputShapeFromFields<Args>, | ||
context: Types['Context'], | ||
info: GraphQLResolveInfo, | ||
error: Error, | ||
) => MaybePromise<ShapeFromTypeParam<Types, Type, Nullable>>; | ||
export type UnauthorizedErrorFn< | ||
Types extends SchemaTypes, | ||
ParentShape, | ||
Args extends InputFieldMap, | ||
> = ( | ||
parent: ParentShape, | ||
args: InputShapeFromFields<Args>, | ||
context: Types['Context'], | ||
info: GraphQLResolveInfo, | ||
result: ForbiddenResult, | ||
) => Error | string; | ||
export interface UnauthorizedOptions< | ||
Types extends SchemaTypes, | ||
ParentShape, | ||
Type extends TypeParam<Types>, | ||
Nullable extends FieldNullability<Type>, | ||
Args extends InputFieldMap, | ||
> { | ||
unauthorizedError?: UnauthorizedErrorFn<Types, ParentShape, Args>; | ||
unauthorizedResolver?: UnauthorizedResolver<Types, ParentShape, Type, Nullable, Args>; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
282251
2470