Skip to content
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

feat: add authorization #1604

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"graphql": "^16.6.0",
"graphql-passport": "^0.6.4",
"graphql-scalars": "^1.20.1",
"graphql-shield": "^7.6.5",
"lodash": "^4.17.21",
"nodemailer": "^6.8.0",
"passport": "^0.6.0",
Expand Down
96 changes: 96 additions & 0 deletions server/permissions/shield.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { GraphQLResolveInfo } from 'graphql'
import { Context } from '../context'
import { shield } from './shield'

// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
class TypeTest {
@shield({
after: () => true,
})
noArgsAfterRule(
_root: Record<string, never>,
_args: { id: string },
_context: Context,
_info: GraphQLResolveInfo
): boolean {
return true
}

@shield({
after: (
_result: boolean,
_root: Record<string, never>,
_args: { id: string },
_context: Context,
_info: GraphQLResolveInfo
) => true,
})
afterRuleWithSameTypeAsResolver(
_root: Record<string, never>,
_args: { id: string },
_context: Context,
_info: GraphQLResolveInfo
): boolean {
return true
}

@shield({
after: (
_result: boolean,
_root: Record<string, never>,
_args: { id: string },
_context: Context,
_info: GraphQLResolveInfo
) => true,
})
afterRuleAcceptingWiderTypeAsArg(
_root: Record<string, never>,
_args: { id: string; other: string },
_context: Context,
_info: GraphQLResolveInfo
): boolean {
return true
}

// @ts-expect-error: rule cannot accept more specialized type than resolver
@shield({
after: (
_result: { id: string },
_root: Record<string, never>,
_args: { id: string; other: string },
_context: Context,
_info: GraphQLResolveInfo
) => true,
})
afterRuleAcceptingInvalidTypeAsArg(
_root: Record<string, never>,
_args: { id: string },
_context: Context,
_info: GraphQLResolveInfo
): boolean {
return true
}
}

class TestResolver {
@shield({
after: (result) => result === 'verified',
})
verify(_root: Record<string, never>, { status }: { status: string }) {
return status
}
}

describe('after rule', () => {
it('do not modify resolver result if rule returns true', () => {
const resolver = new TestResolver()
const result = resolver.verify({} as never, { status: 'verified' })
expect(result).toEqual('verified')
})
it('throw error if rule returns false', () => {
const resolver = new TestResolver()
expect(
resolver.verify({} as never, { status: 'not verified' })
).toThrowError()
})
})
92 changes: 92 additions & 0 deletions server/permissions/shield.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { GraphQLResolveInfo } from 'graphql'
import { IRuleResult, ShieldRule } from 'graphql-shield/typings/types'

export type ResolverFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => TResult

// GraphQL Shield doesn't support output rules yet, https://github.com/dimatill/graphql-shield/issues/1210
type AfterRule<TResult, TParent, TContext, TArgs> = (
result: TResult,
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => Promise<IRuleResult> | IRuleResult

function then<T, V>(
value: PromiseOrValue<T>,
onValue: (t: T, sync?: true) => PromiseOrValue<V>,
onError: (error: any) => PromiseOrValue<V> = (e) => {
throw e
}
): PromiseOrValue<V> {
if (value instanceof Promise) {
return value.then(onValue).catch(onError)
}
try {
return onValue(value, true)
} catch (e) {
return onError(e)
}
}

export function shield<TResult, TParent, TContext, TArgs>({
before,
after,
}: {
before?: ShieldRule
// TODO: Bind the return type of the resolver to the type of the rule
after?: AfterRule<any, TParent, TContext, TArgs>
}): <
Result extends TResult,
Parent extends TParent,
Context extends TContext,
Args extends TArgs
>(
_target: object,
_key: string | symbol,
descriptor: TypedPropertyDescriptor<ResolverFn<Result, Parent, Context, Args>>
) => void {
return function (_target, _key, descriptor) {
const original = descriptor.value
if (!original) {
throw new Error('No original function')
}

// @ts-expect-error: We don't really change the type of the function, we just wrap it.
descriptor.value = function (parent, args, context, info) {
return then(
before
? // @ts-expect-error: Graphql-shield does not provide proper types for rules.
// https://github.com/dimatill/graphql-shield/issues/1479
before.resolve(parent, args, context, info, {})
: true,
(result) => {
if (result === true) {
return then(
original.apply(this, [parent, args, context, info]),
(value) => {
return then(
after ? after(value, parent, args, context, info) : true,
(resultAfter) => {
if (resultAfter === true) {
return value
} else {
return resultAfter
}
}
)
}
)
} else {
return result
}
}
)
}
}
}
77 changes: 76 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,15 @@ __metadata:
languageName: node
linkType: hard

"@babel/runtime@npm:^7.15.4":
version: 7.20.1
resolution: "@babel/runtime@npm:7.20.1"
dependencies:
regenerator-runtime: ^0.13.10
checksum: 00567a333d3357925742a6f5e39394dcc0af6e6029103fe188158bf7ae8b0b3ee3c6c0f68fccc217f0a6cfa455f6be252298baf56b3f5ff37b34313b170cd9f6
languageName: node
linkType: hard

"@babel/standalone@npm:^7.19.0":
version: 7.19.5
resolution: "@babel/standalone@npm:7.19.5"
Expand Down Expand Up @@ -6162,7 +6171,7 @@ __metadata:
languageName: node
linkType: hard

"@types/lodash@npm:^4.14.190":
"@types/lodash@npm:^4.14.175, @types/lodash@npm:^4.14.190":
version: 4.14.190
resolution: "@types/lodash@npm:4.14.190"
checksum: 353a55a1222a57224ead8bd3379fabb2e3e3f6685f599c48b1b66f099d8e9e015e6c6352c6e2275954004c3bb510377804c63117233c6f187700b6ab78adfa02
Expand Down Expand Up @@ -6558,6 +6567,13 @@ __metadata:
languageName: node
linkType: hard

"@types/yup@npm:0.29.13":
version: 0.29.13
resolution: "@types/yup@npm:0.29.13"
checksum: 042eb51becd38559d0d7c86a40e0118d84d2b071c926999ec7721cbef759673be290d69b35ca7985db1bbb6d97544a2db20bb7cacbdaf827391bdd199e3789f9
languageName: node
linkType: hard

"@typescript-eslint/eslint-plugin@npm:^5.42.1":
version: 5.44.0
resolution: "@typescript-eslint/eslint-plugin@npm:5.44.0"
Expand Down Expand Up @@ -14167,6 +14183,21 @@ __metadata:
languageName: node
linkType: hard

"graphql-shield@npm:^7.6.5":
version: 7.6.5
resolution: "graphql-shield@npm:7.6.5"
dependencies:
"@types/yup": 0.29.13
object-hash: ^3.0.0
tslib: ^2.4.0
yup: ^0.32.0
peerDependencies:
graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
graphql-middleware: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^6.0.0
checksum: fb8ffd32838699a18f9560304be1eb1c042cc32423ff2b4b15225249f7687eff179ba61363c2c8f215a926533ebb09b823c83b56fdc18fc6a4e7e156bb74d45c
languageName: node
linkType: hard

"graphql-tag@npm:^2.11.0, graphql-tag@npm:^2.12.6":
version: 2.12.6
resolution: "graphql-tag@npm:2.12.6"
Expand Down Expand Up @@ -16094,6 +16125,7 @@ __metadata:
graphql-codegen-typescript-validation-schema: ^0.7.0
graphql-passport: ^0.6.4
graphql-scalars: ^1.20.1
graphql-shield: ^7.6.5
lodash: ^4.17.21
mount-vue-component: ^0.10.2
naive-ui: ^2.34.2
Expand Down Expand Up @@ -18463,6 +18495,13 @@ __metadata:
languageName: node
linkType: hard

"nanoclone@npm:^0.2.1":
version: 0.2.1
resolution: "nanoclone@npm:0.2.1"
checksum: 96b2954e22f70561f41e20d69856266c65583c2a441dae108f1dc71b716785d2c8038dac5f1d5e92b117aed3825f526b53139e2e5d6e6db8a77cfa35b3b8bf40
languageName: node
linkType: hard

"nanoid@npm:^3.3.1, nanoid@npm:^3.3.4":
version: 3.3.4
resolution: "nanoid@npm:3.3.4"
Expand Down Expand Up @@ -20854,6 +20893,13 @@ __metadata:
languageName: node
linkType: hard

"property-expr@npm:^2.0.4":
version: 2.0.5
resolution: "property-expr@npm:2.0.5"
checksum: 4ebe82ce45aaf1527e96e2ab84d75d25217167ec3ff6378cf83a84fb4abc746e7c65768a79d275881602ae82f168f9a6dfaa7f5e331d0fcc83d692770bcce5f1
languageName: node
linkType: hard

"property-information@npm:^5.0.0, property-information@npm:^5.3.0":
version: 5.6.0
resolution: "property-information@npm:5.6.0"
Expand Down Expand Up @@ -21554,6 +21600,13 @@ __metadata:
languageName: node
linkType: hard

"regenerator-runtime@npm:^0.13.10":
version: 0.13.11
resolution: "regenerator-runtime@npm:0.13.11"
checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4
languageName: node
linkType: hard

"regenerator-runtime@npm:^0.13.4, regenerator-runtime@npm:^0.13.7":
version: 0.13.10
resolution: "regenerator-runtime@npm:0.13.10"
Expand Down Expand Up @@ -24009,6 +24062,13 @@ __metadata:
languageName: node
linkType: hard

"toposort@npm:^2.0.2":
version: 2.0.2
resolution: "toposort@npm:2.0.2"
checksum: d64c74b570391c9432873f48e231b439ee56bc49f7cb9780b505cfdf5cb832f808d0bae072515d93834dd6bceca5bb34448b5b4b408335e4d4716eaf68195dcb
languageName: node
linkType: hard

"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
Expand Down Expand Up @@ -26462,6 +26522,21 @@ __metadata:
languageName: node
linkType: hard

"yup@npm:^0.32.0":
version: 0.32.11
resolution: "yup@npm:0.32.11"
dependencies:
"@babel/runtime": ^7.15.4
"@types/lodash": ^4.14.175
lodash: ^4.17.21
lodash-es: ^4.17.21
nanoclone: ^0.2.1
property-expr: ^2.0.4
toposort: ^2.0.2
checksum: 43a16786b47cc910fed4891cebdd89df6d6e31702e9462e8f969c73eac88551ce750732608012201ea6b93802c8847cb0aa27b5d57370640f4ecf30f9f97d4b0
languageName: node
linkType: hard

"zen-observable-ts@npm:^1.1.0, zen-observable-ts@npm:^1.2.5":
version: 1.2.5
resolution: "zen-observable-ts@npm:1.2.5"
Expand Down