@makerx/graphql-core 3.0.0-beta.2 → 3.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -15
- package/index.d.ts +1 -0
- package/index.js +4 -0
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/package.json +9 -6
- package/shield.d.ts +89 -10
- package/shield.js +87 -10
- package/shield.js.map +1 -1
- package/shield.mjs +84 -12
- package/shield.mjs.map +1 -1
- package/unauthorised.d.ts +30 -0
- package/unauthorised.js +45 -0
- package/unauthorised.js.map +1 -0
- package/unauthorised.mjs +41 -0
- package/unauthorised.mjs.map +1 -0
package/README.md
CHANGED
|
@@ -91,21 +91,21 @@ const graphqlServer = createServer({
|
|
|
91
91
|
|
|
92
92
|
`context.requestInfo` is built for every request — both HTTP and websocket subscription connects — so downstream code can distinguish sources, rebuild URLs, pass through correlation headers, etc.
|
|
93
93
|
|
|
94
|
-
| Field | Description
|
|
95
|
-
| --------------- |
|
|
96
|
-
| `requestId` | `x-request-id` header if present, otherwise a freshly generated UUID.
|
|
97
|
-
| `source` | `'http'` for regular requests, `'subscription'` for websocket connects.
|
|
98
|
-
| `protocol` | `'http'` / `'https'` for HTTP, `'ws'` / `'wss'` for subscriptions (resolved via `x-forwarded-proto` or TLS socket encryption).
|
|
99
|
-
| `host` | Hostname only (no port). Prefers `x-forwarded-host`, falls back to the `Host` header, then `req.hostname` (Express only).
|
|
100
|
-
| `port` | Port parsed from `x-forwarded-host` / `Host` header when present; `undefined` otherwise.
|
|
94
|
+
| Field | Description |
|
|
95
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
96
|
+
| `requestId` | `x-request-id` header if present, otherwise a freshly generated UUID. |
|
|
97
|
+
| `source` | `'http'` for regular requests, `'subscription'` for websocket connects. |
|
|
98
|
+
| `protocol` | `'http'` / `'https'` for HTTP, `'ws'` / `'wss'` for subscriptions (resolved via `x-forwarded-proto` or TLS socket encryption). |
|
|
99
|
+
| `host` | Hostname only (no port). Prefers `x-forwarded-host`, falls back to the `Host` header, then `req.hostname` (Express only). |
|
|
100
|
+
| `port` | Port parsed from `x-forwarded-host` / `Host` header when present; `undefined` otherwise. |
|
|
101
101
|
| `baseUrl` | Fully-qualified origin (`scheme://host[:port]`) with default ports stripped. For subscriptions the scheme is normalised to `http(s)` so the value composes with relative URLs. |
|
|
102
|
-
| `url` | `req.originalUrl` for HTTP, `req.url` for subscription connects.
|
|
103
|
-
| `origin` | `Origin` header.
|
|
104
|
-
| `referer` | `Referer` header.
|
|
105
|
-
| `correlationId` | `x-correlation-id` header.
|
|
106
|
-
| `arrLogId` | `x-arr-log-id` header (Azure Front Door / ARR).
|
|
107
|
-
| `clientIp` | First value from `x-forwarded-for`, falling back to `socket.remoteAddress`.
|
|
108
|
-
| `userAgent` | `User-Agent` header.
|
|
102
|
+
| `url` | `req.originalUrl` for HTTP, `req.url` for subscription connects. |
|
|
103
|
+
| `origin` | `Origin` header. |
|
|
104
|
+
| `referer` | `Referer` header. |
|
|
105
|
+
| `correlationId` | `x-correlation-id` header. |
|
|
106
|
+
| `arrLogId` | `x-arr-log-id` header (Azure Front Door / ARR). |
|
|
107
|
+
| `clientIp` | First value from `x-forwarded-for`, falling back to `socket.remoteAddress`. |
|
|
108
|
+
| `userAgent` | `User-Agent` header. |
|
|
109
109
|
|
|
110
110
|
You can add more via `augmentRequestInfo(input)`. Lambda deployments also get `functionName` and `awsRequestId` when a `LambdaContext` is supplied.
|
|
111
111
|
|
|
@@ -163,6 +163,112 @@ const graphqlServer = createServer({
|
|
|
163
163
|
})
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
## Shield (authorization)
|
|
167
|
+
|
|
168
|
+
The `shield` module provides typed helpers around [graphql-shield](https://the-guild.dev/graphql/shield) for defining authorization middleware against your generated `Resolvers` type.
|
|
169
|
+
|
|
170
|
+
`graphql-shield` is an optional peer dependency — install it only if you use this module:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
npm i graphql-shield
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### createShieldSchema
|
|
177
|
+
|
|
178
|
+
Builds a shield middleware with a schema typed against your resolver map, so field-level rule wiring is checked by the TypeScript compiler.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { Resolvers } from '../generated/types'
|
|
182
|
+
import { or } from 'graphql-shield'
|
|
183
|
+
import { applyMiddleware } from 'graphql-middleware'
|
|
184
|
+
import { createShieldSchema, hasRoleRule, unauthorisedError } from '@makerx/graphql-core'
|
|
185
|
+
|
|
186
|
+
const isCoordinator = hasRoleRule('coordinator')
|
|
187
|
+
const isSystemAdmin = hasRoleRule('system-admin')
|
|
188
|
+
const isAuthorisedUser = or(isCoordinator, isSystemAdmin)
|
|
189
|
+
|
|
190
|
+
const shieldSchema = createShieldSchema<Resolvers>(
|
|
191
|
+
{
|
|
192
|
+
Query: {
|
|
193
|
+
'*': isAuthorisedUser,
|
|
194
|
+
user: isCoordinator,
|
|
195
|
+
},
|
|
196
|
+
Mutation: {
|
|
197
|
+
'*': isCoordinator,
|
|
198
|
+
createUser: isSystemAdmin,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{ fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError() },
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const schema = applyMiddleware(makeExecutableSchema({ ... }), shieldSchema)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The `'*'` key at each object level is a fallback rule applied to any field at that level without an explicit rule.
|
|
208
|
+
|
|
209
|
+
The wrapper defaults `allowExternalErrors: true`; the full shield options object (second argument) is forwarded to `shield(...)` and can override it.
|
|
210
|
+
|
|
211
|
+
#### v3 breaking change: no default fallback rule
|
|
212
|
+
|
|
213
|
+
Prior to v3 this wrapper defaulted `fallbackRule` to `allow` — fields not covered by the schema were open by default. v3 removes that default and the wrapper no longer makes that choice for you. Without an explicit `fallbackRule`, graphql-shield's own default (`allow`) still applies, so behaviour is unchanged when no options are passed, but if you want a deny-by-default posture you should now pass an explicit `fallbackRule` (commonly your auth-check rule) along with a `fallbackError`.
|
|
214
|
+
|
|
215
|
+
The second argument also changed from a bare `ShieldRule` to the full shield options object:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// v2
|
|
219
|
+
createShieldSchema<Resolvers>(schema, isAuthorisedUser)
|
|
220
|
+
|
|
221
|
+
// v3
|
|
222
|
+
createShieldSchema<Resolvers>(schema, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError() })
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Role and scope rules
|
|
226
|
+
|
|
227
|
+
Convenience builders that read `ctx.user.roles` / `ctx.user.scopes`. All use graphql-shield's `'contextual'` cache so the result is reused across fields within a request.
|
|
228
|
+
|
|
229
|
+
| Helper | Description |
|
|
230
|
+
| ---------------------------------------- | ----------------------------------------------------------------------------- |
|
|
231
|
+
| `hasRoleRule(role, ruleName?)` | Passes when `user.roles` includes `role`. |
|
|
232
|
+
| `hasAnyRoleRule(...roles)` | Passes when `user.roles` includes at least one of `roles`. |
|
|
233
|
+
| `hasAnyRoleRuleWithName(name, ...roles)` | Same, with an explicit rule name / cache key for large or dynamic role lists. |
|
|
234
|
+
| `hasScopeRule(scope)` | Passes when `user.scopes` includes `scope`. |
|
|
235
|
+
| `hasAnyScopeRule(...scopes)` | Passes when `user.scopes` includes at least one of `scopes`. |
|
|
236
|
+
|
|
237
|
+
### createRule
|
|
238
|
+
|
|
239
|
+
Typed wrapper around graphql-shield's `rule(...)` for ad-hoc rules. Unlike the underlying `rule`, `createRule` types `parent`, `args` and `ctx` to your generics.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const isOwner = createRule<GraphQLContext, Record, { ownerId: string }>((parent, _, ctx) => parent.ownerId === ctx.user?.id)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Defaults to `'strict'` cache; pass `'contextual'` as the second argument for rules whose result depends only on `ctx`.
|
|
246
|
+
|
|
247
|
+
### combineRuleWithAll
|
|
248
|
+
|
|
249
|
+
Composes a rule with every rule in an existing shield schema via a combinator (`and` | `or` | `chain` | `race`). Useful for post-hoc gating — e.g. bolting a "not blocked" check onto an existing schema without touching each field:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { chain } from 'graphql-shield'
|
|
253
|
+
|
|
254
|
+
const gated = combineRuleWithAll(shieldSchema, accountNotBlocked, chain)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Authorization errors
|
|
258
|
+
|
|
259
|
+
The `unauthorised` module provides shared helpers for producing forbidden errors used by the shield wrapper and by resolvers.
|
|
260
|
+
|
|
261
|
+
| Helper | Description |
|
|
262
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
263
|
+
| `unauthorisedError(message?)` | Builds a `GraphQLError` with Apollo's `FORBIDDEN` extension code and HTTP 403 status. |
|
|
264
|
+
| `throwUnauthorised(message?)` | Throws `unauthorisedError`; return type is `never` so TypeScript narrows control flow past the call. |
|
|
265
|
+
| `permissionInvariant(condition, message?)` | Assertion helper: throws `unauthorisedError` when `condition` is false, otherwise narrows it to `true`. |
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
permissionInvariant(ctx.user?.id === record.ownerId, 'Only the owner can edit this record')
|
|
269
|
+
// past this line, TypeScript knows the condition held
|
|
270
|
+
```
|
|
271
|
+
|
|
166
272
|
## Logging
|
|
167
273
|
|
|
168
274
|
### logGraphQLOperation
|
|
@@ -223,7 +329,6 @@ This library includes a `subscriptions` module to provide simple setup using the
|
|
|
223
329
|
1. Create a subscriptions server, using the ws-server cleanup function in your server lifecycle.
|
|
224
330
|
|
|
225
331
|
The `useSubscriptionsServer` function sets up:
|
|
226
|
-
|
|
227
332
|
- Auth token validation as part of establishing (or rejecting) the connection (behaviour defined by `verifyToken` and `requireAuth` args)
|
|
228
333
|
- GraphQL context creation
|
|
229
334
|
- Logging from the server `onConnect`, `onDisconnect`, `onOperation`, `onNext` and `onError` callbacks
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var context = require('./context.js');
|
|
|
4
4
|
var logging = require('./logging.js');
|
|
5
5
|
var requestInfo = require('./request-info.js');
|
|
6
6
|
var schemaUtil = require('./schema-util.js');
|
|
7
|
+
var unauthorised = require('./unauthorised.js');
|
|
7
8
|
var User = require('./User.js');
|
|
8
9
|
var utils = require('./utils.js');
|
|
9
10
|
|
|
@@ -18,6 +19,9 @@ exports.buildConnectRequestInfo = requestInfo.buildConnectRequestInfo;
|
|
|
18
19
|
exports.connectRequestBaseUrl = requestInfo.connectRequestBaseUrl;
|
|
19
20
|
exports.requestBaseUrl = requestInfo.requestBaseUrl;
|
|
20
21
|
exports.requireExplicitResolversForScalars = schemaUtil.requireExplicitResolversForScalars;
|
|
22
|
+
exports.permissionInvariant = unauthorised.permissionInvariant;
|
|
23
|
+
exports.throwUnauthorised = unauthorised.throwUnauthorised;
|
|
24
|
+
exports.unauthorisedError = unauthorised.unauthorisedError;
|
|
21
25
|
exports.User = User.User;
|
|
22
26
|
exports.isIntrospectionQuery = utils.isIntrospectionQuery;
|
|
23
27
|
exports.isNil = utils.isNil;
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ export { createContextFactory, defaultCreateUser } from './context.mjs';
|
|
|
2
2
|
export { logGraphQLOperation, logSubscriptionOperation } from './logging.mjs';
|
|
3
3
|
export { buildBaseRequestInfo, buildConnectRequestInfo, connectRequestBaseUrl, requestBaseUrl } from './request-info.mjs';
|
|
4
4
|
export { requireExplicitResolversForScalars } from './schema-util.mjs';
|
|
5
|
+
export { permissionInvariant, throwUnauthorised, unauthorisedError } from './unauthorised.mjs';
|
|
5
6
|
export { User } from './User.mjs';
|
|
6
7
|
export { isIntrospectionQuery, isNil, isNotNil } from './utils.mjs';
|
|
7
8
|
//# sourceMappingURL=index.mjs.map
|
package/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"**"
|
|
5
5
|
],
|
|
6
6
|
"name": "@makerx/graphql-core",
|
|
7
|
-
"version": "3.0.0-beta.
|
|
7
|
+
"version": "3.0.0-beta.4",
|
|
8
8
|
"private": false,
|
|
9
9
|
"description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs",
|
|
10
10
|
"author": "MakerX",
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@makerx/node-common": ">=1",
|
|
25
25
|
"es-toolkit": ">=1",
|
|
26
|
-
"express": "
|
|
27
|
-
"graphql": "
|
|
28
|
-
"graphql-shield": "
|
|
29
|
-
"graphql-ws": "
|
|
30
|
-
"ws": "
|
|
26
|
+
"express": ">=4",
|
|
27
|
+
"graphql": ">=16",
|
|
28
|
+
"graphql-shield": ">=7",
|
|
29
|
+
"graphql-ws": ">=5",
|
|
30
|
+
"ws": ">=8"
|
|
31
31
|
},
|
|
32
32
|
"peerDependenciesMeta": {
|
|
33
|
+
"graphql-shield": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
33
36
|
"graphql-ws": {
|
|
34
37
|
"optional": true
|
|
35
38
|
},
|
package/shield.d.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
import type { and, chain, or, race } from 'graphql-shield';
|
|
2
|
-
import {
|
|
1
|
+
import type { allow, and, chain, or, race } from 'graphql-shield';
|
|
2
|
+
import { shield } from 'graphql-shield';
|
|
3
3
|
import type { Primitive } from './type-utils';
|
|
4
|
+
export type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>;
|
|
4
5
|
type RuleCombinator = typeof chain | typeof race | typeof or | typeof and;
|
|
5
6
|
export type ShieldRule = ReturnType<(typeof allow)['getRules']>[number];
|
|
7
|
+
/**
|
|
8
|
+
* Thin typed wrapper around graphql-shield's `rule(...)` for building ad-hoc shield rules.
|
|
9
|
+
*
|
|
10
|
+
* Lets you pass a plain predicate (sync or async, or returning an `Error`) and get back a
|
|
11
|
+
* {@link ShieldRule} with the `parent`, `args`, and `ctx` arguments typed to your generics —
|
|
12
|
+
* graphql-shield's native `rule` types these as `any`.
|
|
13
|
+
*
|
|
14
|
+
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
15
|
+
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
16
|
+
*
|
|
17
|
+
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
18
|
+
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
*
|
|
22
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
23
|
+
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
24
|
+
* )
|
|
25
|
+
*/
|
|
6
26
|
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(logic: (parent: TParent, args: TArgs, ctx: TContext) => boolean | Promise<boolean> | Error, cache?: "contextual" | "strict") => ShieldRule;
|
|
7
27
|
/**
|
|
8
28
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
@@ -18,26 +38,85 @@ export declare function combineRuleWithAll<T>(schema: ShieldSchema<T>, rule: Shi
|
|
|
18
38
|
/**
|
|
19
39
|
* Creates graphql shield middleware making use of generics to type the definition of the rules.
|
|
20
40
|
* @param schema A mapping of schema objects to the shield rules that define the access to that object
|
|
21
|
-
* @param
|
|
41
|
+
* @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.
|
|
22
42
|
*
|
|
23
43
|
* Usage:
|
|
24
44
|
* import { Resolvers } from '../generated/types'
|
|
25
45
|
*
|
|
46
|
+
* const isCoordinator = hasRoleRule(UserRoles.Coordinator)
|
|
47
|
+
* const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)
|
|
48
|
+
* const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)
|
|
49
|
+
*
|
|
50
|
+
* const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)
|
|
51
|
+
* const isDataAuthor = or(isCoordinator, isSystemAdmin)
|
|
52
|
+
*
|
|
26
53
|
* const shieldSchema = createShieldSchema<Resolvers>({
|
|
27
54
|
* Query: {
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
55
|
+
* '*': isAuthorisedUser,
|
|
56
|
+
* user: isCoordinator,
|
|
57
|
+
* findUsers: isCoordinator,
|
|
58
|
+
* },
|
|
59
|
+
* Mutation: {
|
|
60
|
+
* '*': isDataAuthor,
|
|
61
|
+
* createUser: isCoordinator,
|
|
62
|
+
* },
|
|
63
|
+
* // specific rules for mutations, types, fields...
|
|
64
|
+
* }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})
|
|
65
|
+
*
|
|
66
|
+
* const unprotected = makeExecutableSchema({...})
|
|
67
|
+
* return applyMiddleware(unprotected, shieldSchema)
|
|
68
|
+
*/
|
|
69
|
+
export declare function createShieldSchema<TRootResolvers>(schema: ShieldSchema<TRootResolvers>, options?: ShieldOptions): import("graphql-middleware").IMiddlewareGenerator<any, any, any>;
|
|
70
|
+
/**
|
|
71
|
+
* A shield rule tree typed against a resolver map (typically your generated `Resolvers` type).
|
|
32
72
|
*
|
|
33
|
-
*
|
|
73
|
+
* Mirrors the shape of `TResolver` so each type/field can be assigned a {@link ShieldRule}.
|
|
74
|
+
* Object levels may also include a `'*'` key — a fallback rule applied to any field at that
|
|
75
|
+
* level that doesn't have an explicit rule. Leaf (primitive) positions collapse to a single
|
|
76
|
+
* {@link ShieldRule}.
|
|
34
77
|
*
|
|
35
|
-
*
|
|
78
|
+
* Usage:
|
|
79
|
+
*
|
|
80
|
+
* const schema: ShieldSchema<Resolvers> = {
|
|
81
|
+
* Query: {
|
|
82
|
+
* '*': isAuthorisedUser,
|
|
83
|
+
* publicThing: allow,
|
|
84
|
+
* },
|
|
85
|
+
* Mutation: { createUser: isCoordinator },
|
|
86
|
+
* }
|
|
36
87
|
*/
|
|
37
|
-
export declare function createShieldSchema<TRootResolvers>(schema: ShieldSchema<TRootResolvers>, fallbackRule?: ShieldRule): import("graphql-middleware").IMiddlewareGenerator<any, any, any>;
|
|
38
88
|
export type ShieldSchema<TResolver> = TResolver extends Primitive ? ShieldRule : {
|
|
39
89
|
[key in keyof TResolver]?: ShieldSchema<TResolver[key]> | ShieldRule;
|
|
40
90
|
} & {
|
|
41
91
|
'*'?: ShieldRule;
|
|
42
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Shield rule that passes when the current user has the given role.
|
|
95
|
+
* @param role The role the user must have on `ctx.user.roles`.
|
|
96
|
+
* @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function hasRoleRule(role: string, ruleName?: string): ShieldRule;
|
|
99
|
+
/**
|
|
100
|
+
* Shield rule that passes when the current user has at least one of the given roles.
|
|
101
|
+
* Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.
|
|
102
|
+
* @param roles The set of roles, any of which satisfies the rule.
|
|
103
|
+
*/
|
|
104
|
+
export declare function hasAnyRoleRule(...roles: string[]): ShieldRule;
|
|
105
|
+
/**
|
|
106
|
+
* Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.
|
|
107
|
+
* Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.
|
|
108
|
+
* @param ruleName The name (and cache key) for the rule.
|
|
109
|
+
* @param roles The set of roles, any of which satisfies the rule.
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasAnyRoleRuleWithName(ruleName: string, ...roles: string[]): ShieldRule;
|
|
112
|
+
/**
|
|
113
|
+
* Shield rule that passes when the current user has the given scope.
|
|
114
|
+
* @param scope The scope the user must have on `ctx.user.scopes`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function hasScopeRule(scope: string): ShieldRule;
|
|
117
|
+
/**
|
|
118
|
+
* Shield rule that passes when the current user has at least one of the given scopes.
|
|
119
|
+
* @param scopes The set of scopes, any of which satisfies the rule.
|
|
120
|
+
*/
|
|
121
|
+
export declare function hasAnyScopeRule(...scopes: string[]): ShieldRule;
|
|
43
122
|
export {};
|
package/shield.js
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
var graphqlShield = require('graphql-shield');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Thin typed wrapper around graphql-shield's `rule(...)` for building ad-hoc shield rules.
|
|
7
|
+
*
|
|
8
|
+
* Lets you pass a plain predicate (sync or async, or returning an `Error`) and get back a
|
|
9
|
+
* {@link ShieldRule} with the `parent`, `args`, and `ctx` arguments typed to your generics —
|
|
10
|
+
* graphql-shield's native `rule` types these as `any`.
|
|
11
|
+
*
|
|
12
|
+
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
13
|
+
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
14
|
+
*
|
|
15
|
+
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
16
|
+
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
*
|
|
20
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
21
|
+
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
22
|
+
* )
|
|
23
|
+
*/
|
|
5
24
|
const createRule = (logic, cache = 'strict') => graphqlShield.rule({ cache })(async (parent, args, ctx) => logic(parent, args, ctx));
|
|
6
25
|
/**
|
|
7
26
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
@@ -24,30 +43,88 @@ function combineRuleWithAll(schema, rule, combinator) {
|
|
|
24
43
|
/**
|
|
25
44
|
* Creates graphql shield middleware making use of generics to type the definition of the rules.
|
|
26
45
|
* @param schema A mapping of schema objects to the shield rules that define the access to that object
|
|
27
|
-
* @param
|
|
46
|
+
* @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.
|
|
28
47
|
*
|
|
29
48
|
* Usage:
|
|
30
49
|
* import { Resolvers } from '../generated/types'
|
|
31
50
|
*
|
|
51
|
+
* const isCoordinator = hasRoleRule(UserRoles.Coordinator)
|
|
52
|
+
* const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)
|
|
53
|
+
* const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)
|
|
54
|
+
*
|
|
55
|
+
* const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)
|
|
56
|
+
* const isDataAuthor = or(isCoordinator, isSystemAdmin)
|
|
57
|
+
*
|
|
32
58
|
* const shieldSchema = createShieldSchema<Resolvers>({
|
|
33
59
|
* Query: {
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
60
|
+
* '*': isAuthorisedUser,
|
|
61
|
+
* user: isCoordinator,
|
|
62
|
+
* findUsers: isCoordinator,
|
|
63
|
+
* },
|
|
64
|
+
* Mutation: {
|
|
65
|
+
* '*': isDataAuthor,
|
|
66
|
+
* createUser: isCoordinator,
|
|
67
|
+
* },
|
|
68
|
+
* // specific rules for mutations, types, fields...
|
|
69
|
+
* }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})
|
|
40
70
|
*
|
|
41
|
-
*
|
|
71
|
+
* const unprotected = makeExecutableSchema({...})
|
|
72
|
+
* return applyMiddleware(unprotected, shieldSchema)
|
|
42
73
|
*/
|
|
43
|
-
function createShieldSchema(schema,
|
|
74
|
+
function createShieldSchema(schema, options) {
|
|
44
75
|
return graphqlShield.shield(schema, {
|
|
45
76
|
allowExternalErrors: true,
|
|
46
|
-
|
|
77
|
+
...options,
|
|
47
78
|
});
|
|
48
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Shield rule that passes when the current user has the given role.
|
|
82
|
+
* @param role The role the user must have on `ctx.user.roles`.
|
|
83
|
+
* @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.
|
|
84
|
+
*/
|
|
85
|
+
function hasRoleRule(role, ruleName) {
|
|
86
|
+
return graphqlShield.rule(ruleName ?? `hasRole-${role}`, { cache: 'contextual' })((_, __, { user }) => user?.roles.includes(role) === true);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Shield rule that passes when the current user has at least one of the given roles.
|
|
90
|
+
* Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.
|
|
91
|
+
* @param roles The set of roles, any of which satisfies the rule.
|
|
92
|
+
*/
|
|
93
|
+
function hasAnyRoleRule(...roles) {
|
|
94
|
+
return hasAnyRoleRuleWithName(`hasAnyRole-${roles.join(',')}`, ...roles);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.
|
|
98
|
+
* Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.
|
|
99
|
+
* @param ruleName The name (and cache key) for the rule.
|
|
100
|
+
* @param roles The set of roles, any of which satisfies the rule.
|
|
101
|
+
*/
|
|
102
|
+
function hasAnyRoleRuleWithName(ruleName, ...roles) {
|
|
103
|
+
return graphqlShield.rule(ruleName, { cache: 'contextual' })((_, __, { user }) => {
|
|
104
|
+
return user?.roles.some((role) => roles.includes(role)) === true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Shield rule that passes when the current user has the given scope.
|
|
109
|
+
* @param scope The scope the user must have on `ctx.user.scopes`.
|
|
110
|
+
*/
|
|
111
|
+
function hasScopeRule(scope) {
|
|
112
|
+
return graphqlShield.rule(`hasScope-${scope}`, { cache: 'contextual' })((_, __, { user }) => user?.scopes.includes(scope) === true);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Shield rule that passes when the current user has at least one of the given scopes.
|
|
116
|
+
* @param scopes The set of scopes, any of which satisfies the rule.
|
|
117
|
+
*/
|
|
118
|
+
function hasAnyScopeRule(...scopes) {
|
|
119
|
+
return graphqlShield.rule(`hasAnyScope-${scopes.join(',')}`, { cache: 'contextual' })((_, __, { user }) => user?.scopes.some((scope) => scopes.includes(scope)) === true);
|
|
120
|
+
}
|
|
49
121
|
|
|
50
122
|
exports.combineRuleWithAll = combineRuleWithAll;
|
|
51
123
|
exports.createRule = createRule;
|
|
52
124
|
exports.createShieldSchema = createShieldSchema;
|
|
125
|
+
exports.hasAnyRoleRule = hasAnyRoleRule;
|
|
126
|
+
exports.hasAnyRoleRuleWithName = hasAnyRoleRuleWithName;
|
|
127
|
+
exports.hasAnyScopeRule = hasAnyScopeRule;
|
|
128
|
+
exports.hasRoleRule = hasRoleRule;
|
|
129
|
+
exports.hasScopeRule = hasScopeRule;
|
|
53
130
|
//# sourceMappingURL=shield.js.map
|
package/shield.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield.js","sources":["../src/shield.ts"],"sourcesContent":["import type { and, chain, IRules, or, race } from 'graphql-shield'\nimport {
|
|
1
|
+
{"version":3,"file":"shield.js","sources":["../src/shield.ts"],"sourcesContent":["import type { allow, and, chain, IRules, or, race } from 'graphql-shield'\nimport { rule, shield } from 'graphql-shield'\nimport type { GraphQLContext } from './context'\nimport type { Primitive } from './type-utils'\n\nexport type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>\n\ntype RuleCombinator = typeof chain | typeof race | typeof or | typeof and\n// For whatever reason, graphql-shield doesn't export this type, but we can extract if from\n// what it does export.\nexport type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]\n\n/**\n * Thin typed wrapper around graphql-shield's `rule(...)` for building ad-hoc shield rules.\n *\n * Lets you pass a plain predicate (sync or async, or returning an `Error`) and get back a\n * {@link ShieldRule} with the `parent`, `args`, and `ctx` arguments typed to your generics —\n * graphql-shield's native `rule` types these as `any`.\n *\n * Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on\n * `ctx` (e.g. the current user), so it can be reused across fields in the same request.\n *\n * @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.\n * @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.\n *\n * Usage:\n *\n * const isOwner = createRule<GraphQLContext, { ownerId: string }>(\n * (parent, _, ctx) => parent.ownerId === ctx.user?.id,\n * )\n */\nexport const createRule = <TContext, TParent = unknown, TArgs = unknown>(\n logic: (parent: TParent, args: TArgs, ctx: TContext) => boolean | Promise<boolean> | Error,\n cache: 'contextual' | 'strict' = 'strict',\n): ShieldRule => rule({ cache })(async (parent, args, ctx) => logic(parent, args, ctx))\n\n/**\n * Combines a given rule with all existing defined rules using the provided combinator\n * @param schema The existing shield schema with rules\n * @param rule The rule to be combined with existing rules\n * @param combinator The combinator to use. Eg. and | or | chain | race\n *\n * Usage:\n *\n * const updatedSchema = combineRuleWithAll(shieldSchema, rules.accountNotBlocked, chain)\n */\nexport function combineRuleWithAll<T>(schema: ShieldSchema<T>, rule: ShieldRule, combinator: RuleCombinator): ShieldSchema<T> {\n return Object.fromEntries(\n Object.entries(schema).map(([key, value]) => {\n if (value.constructor.name !== 'Object') {\n return [key, combinator(rule, value)]\n }\n return [key, combineRuleWithAll(value, rule, combinator)]\n }),\n ) as ShieldSchema<T>\n}\n\n/**\n * Creates graphql shield middleware making use of generics to type the definition of the rules.\n * @param schema A mapping of schema objects to the shield rules that define the access to that object\n * @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.\n *\n * Usage:\n * import { Resolvers } from '../generated/types'\n *\n * const isCoordinator = hasRoleRule(UserRoles.Coordinator)\n * const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)\n * const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)\n *\n * const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)\n * const isDataAuthor = or(isCoordinator, isSystemAdmin)\n *\n * const shieldSchema = createShieldSchema<Resolvers>({\n * Query: {\n * '*': isAuthorisedUser,\n * user: isCoordinator,\n * findUsers: isCoordinator,\n * },\n * Mutation: {\n * '*': isDataAuthor,\n * createUser: isCoordinator,\n * },\n * // specific rules for mutations, types, fields...\n * }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})\n *\n * const unprotected = makeExecutableSchema({...})\n * return applyMiddleware(unprotected, shieldSchema)\n */\nexport function createShieldSchema<TRootResolvers>(schema: ShieldSchema<TRootResolvers>, options?: ShieldOptions) {\n return shield(schema as IRules, {\n allowExternalErrors: true,\n ...options,\n })\n}\n\n/**\n * A shield rule tree typed against a resolver map (typically your generated `Resolvers` type).\n *\n * Mirrors the shape of `TResolver` so each type/field can be assigned a {@link ShieldRule}.\n * Object levels may also include a `'*'` key — a fallback rule applied to any field at that\n * level that doesn't have an explicit rule. Leaf (primitive) positions collapse to a single\n * {@link ShieldRule}.\n *\n * Usage:\n *\n * const schema: ShieldSchema<Resolvers> = {\n * Query: {\n * '*': isAuthorisedUser,\n * publicThing: allow,\n * },\n * Mutation: { createUser: isCoordinator },\n * }\n */\nexport type ShieldSchema<TResolver> = TResolver extends Primitive\n ? ShieldRule\n : {\n [key in keyof TResolver]?: ShieldSchema<TResolver[key]> | ShieldRule\n } & {\n '*'?: ShieldRule\n }\n\n/**\n * Shield rule that passes when the current user has the given role.\n * @param role The role the user must have on `ctx.user.roles`.\n * @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.\n */\nexport function hasRoleRule(role: string, ruleName?: string): ShieldRule {\n return rule(ruleName ?? `hasRole-${role}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.roles.includes(role) === true,\n )\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles.\n * Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRule(...roles: string[]): ShieldRule {\n return hasAnyRoleRuleWithName(`hasAnyRole-${roles.join(',')}`, ...roles)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.\n * Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.\n * @param ruleName The name (and cache key) for the rule.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRuleWithName(ruleName: string, ...roles: string[]): ShieldRule {\n return rule(ruleName, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => {\n return user?.roles.some((role: string) => roles.includes(role)) === true\n })\n}\n\n/**\n * Shield rule that passes when the current user has the given scope.\n * @param scope The scope the user must have on `ctx.user.scopes`.\n */\nexport function hasScopeRule(scope: string): ShieldRule {\n return rule(`hasScope-${scope}`, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => user?.scopes.includes(scope) === true)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given scopes.\n * @param scopes The set of scopes, any of which satisfies the rule.\n */\nexport function hasAnyScopeRule(...scopes: string[]): ShieldRule {\n return rule(`hasAnyScope-${scopes.join(',')}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.scopes.some((scope: string) => scopes.includes(scope)) === true,\n )\n}\n"],"names":["rule","shield"],"mappings":"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"}
|
package/shield.mjs
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rule, shield } from 'graphql-shield';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Thin typed wrapper around graphql-shield's `rule(...)` for building ad-hoc shield rules.
|
|
5
|
+
*
|
|
6
|
+
* Lets you pass a plain predicate (sync or async, or returning an `Error`) and get back a
|
|
7
|
+
* {@link ShieldRule} with the `parent`, `args`, and `ctx` arguments typed to your generics —
|
|
8
|
+
* graphql-shield's native `rule` types these as `any`.
|
|
9
|
+
*
|
|
10
|
+
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
11
|
+
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
12
|
+
*
|
|
13
|
+
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
14
|
+
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
*
|
|
18
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
19
|
+
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
20
|
+
* )
|
|
21
|
+
*/
|
|
3
22
|
const createRule = (logic, cache = 'strict') => rule({ cache })(async (parent, args, ctx) => logic(parent, args, ctx));
|
|
4
23
|
/**
|
|
5
24
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
@@ -22,28 +41,81 @@ function combineRuleWithAll(schema, rule, combinator) {
|
|
|
22
41
|
/**
|
|
23
42
|
* Creates graphql shield middleware making use of generics to type the definition of the rules.
|
|
24
43
|
* @param schema A mapping of schema objects to the shield rules that define the access to that object
|
|
25
|
-
* @param
|
|
44
|
+
* @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.
|
|
26
45
|
*
|
|
27
46
|
* Usage:
|
|
28
47
|
* import { Resolvers } from '../generated/types'
|
|
29
48
|
*
|
|
49
|
+
* const isCoordinator = hasRoleRule(UserRoles.Coordinator)
|
|
50
|
+
* const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)
|
|
51
|
+
* const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)
|
|
52
|
+
*
|
|
53
|
+
* const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)
|
|
54
|
+
* const isDataAuthor = or(isCoordinator, isSystemAdmin)
|
|
55
|
+
*
|
|
30
56
|
* const shieldSchema = createShieldSchema<Resolvers>({
|
|
31
57
|
* Query: {
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
58
|
+
* '*': isAuthorisedUser,
|
|
59
|
+
* user: isCoordinator,
|
|
60
|
+
* findUsers: isCoordinator,
|
|
61
|
+
* },
|
|
62
|
+
* Mutation: {
|
|
63
|
+
* '*': isDataAuthor,
|
|
64
|
+
* createUser: isCoordinator,
|
|
65
|
+
* },
|
|
66
|
+
* // specific rules for mutations, types, fields...
|
|
67
|
+
* }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})
|
|
38
68
|
*
|
|
39
|
-
*
|
|
69
|
+
* const unprotected = makeExecutableSchema({...})
|
|
70
|
+
* return applyMiddleware(unprotected, shieldSchema)
|
|
40
71
|
*/
|
|
41
|
-
function createShieldSchema(schema,
|
|
72
|
+
function createShieldSchema(schema, options) {
|
|
42
73
|
return shield(schema, {
|
|
43
74
|
allowExternalErrors: true,
|
|
44
|
-
|
|
75
|
+
...options,
|
|
45
76
|
});
|
|
46
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Shield rule that passes when the current user has the given role.
|
|
80
|
+
* @param role The role the user must have on `ctx.user.roles`.
|
|
81
|
+
* @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.
|
|
82
|
+
*/
|
|
83
|
+
function hasRoleRule(role, ruleName) {
|
|
84
|
+
return rule(ruleName ?? `hasRole-${role}`, { cache: 'contextual' })((_, __, { user }) => user?.roles.includes(role) === true);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Shield rule that passes when the current user has at least one of the given roles.
|
|
88
|
+
* Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.
|
|
89
|
+
* @param roles The set of roles, any of which satisfies the rule.
|
|
90
|
+
*/
|
|
91
|
+
function hasAnyRoleRule(...roles) {
|
|
92
|
+
return hasAnyRoleRuleWithName(`hasAnyRole-${roles.join(',')}`, ...roles);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.
|
|
96
|
+
* Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.
|
|
97
|
+
* @param ruleName The name (and cache key) for the rule.
|
|
98
|
+
* @param roles The set of roles, any of which satisfies the rule.
|
|
99
|
+
*/
|
|
100
|
+
function hasAnyRoleRuleWithName(ruleName, ...roles) {
|
|
101
|
+
return rule(ruleName, { cache: 'contextual' })((_, __, { user }) => {
|
|
102
|
+
return user?.roles.some((role) => roles.includes(role)) === true;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Shield rule that passes when the current user has the given scope.
|
|
107
|
+
* @param scope The scope the user must have on `ctx.user.scopes`.
|
|
108
|
+
*/
|
|
109
|
+
function hasScopeRule(scope) {
|
|
110
|
+
return rule(`hasScope-${scope}`, { cache: 'contextual' })((_, __, { user }) => user?.scopes.includes(scope) === true);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Shield rule that passes when the current user has at least one of the given scopes.
|
|
114
|
+
* @param scopes The set of scopes, any of which satisfies the rule.
|
|
115
|
+
*/
|
|
116
|
+
function hasAnyScopeRule(...scopes) {
|
|
117
|
+
return rule(`hasAnyScope-${scopes.join(',')}`, { cache: 'contextual' })((_, __, { user }) => user?.scopes.some((scope) => scopes.includes(scope)) === true);
|
|
118
|
+
}
|
|
47
119
|
|
|
48
|
-
export { combineRuleWithAll, createRule, createShieldSchema };
|
|
120
|
+
export { combineRuleWithAll, createRule, createShieldSchema, hasAnyRoleRule, hasAnyRoleRuleWithName, hasAnyScopeRule, hasRoleRule, hasScopeRule };
|
|
49
121
|
//# sourceMappingURL=shield.mjs.map
|
package/shield.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield.mjs","sources":["../src/shield.ts"],"sourcesContent":["import type { and, chain, IRules, or, race } from 'graphql-shield'\nimport {
|
|
1
|
+
{"version":3,"file":"shield.mjs","sources":["../src/shield.ts"],"sourcesContent":["import type { allow, and, chain, IRules, or, race } from 'graphql-shield'\nimport { rule, shield } from 'graphql-shield'\nimport type { GraphQLContext } from './context'\nimport type { Primitive } from './type-utils'\n\nexport type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>\n\ntype RuleCombinator = typeof chain | typeof race | typeof or | typeof and\n// For whatever reason, graphql-shield doesn't export this type, but we can extract if from\n// what it does export.\nexport type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]\n\n/**\n * Thin typed wrapper around graphql-shield's `rule(...)` for building ad-hoc shield rules.\n *\n * Lets you pass a plain predicate (sync or async, or returning an `Error`) and get back a\n * {@link ShieldRule} with the `parent`, `args`, and `ctx` arguments typed to your generics —\n * graphql-shield's native `rule` types these as `any`.\n *\n * Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on\n * `ctx` (e.g. the current user), so it can be reused across fields in the same request.\n *\n * @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.\n * @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.\n *\n * Usage:\n *\n * const isOwner = createRule<GraphQLContext, { ownerId: string }>(\n * (parent, _, ctx) => parent.ownerId === ctx.user?.id,\n * )\n */\nexport const createRule = <TContext, TParent = unknown, TArgs = unknown>(\n logic: (parent: TParent, args: TArgs, ctx: TContext) => boolean | Promise<boolean> | Error,\n cache: 'contextual' | 'strict' = 'strict',\n): ShieldRule => rule({ cache })(async (parent, args, ctx) => logic(parent, args, ctx))\n\n/**\n * Combines a given rule with all existing defined rules using the provided combinator\n * @param schema The existing shield schema with rules\n * @param rule The rule to be combined with existing rules\n * @param combinator The combinator to use. Eg. and | or | chain | race\n *\n * Usage:\n *\n * const updatedSchema = combineRuleWithAll(shieldSchema, rules.accountNotBlocked, chain)\n */\nexport function combineRuleWithAll<T>(schema: ShieldSchema<T>, rule: ShieldRule, combinator: RuleCombinator): ShieldSchema<T> {\n return Object.fromEntries(\n Object.entries(schema).map(([key, value]) => {\n if (value.constructor.name !== 'Object') {\n return [key, combinator(rule, value)]\n }\n return [key, combineRuleWithAll(value, rule, combinator)]\n }),\n ) as ShieldSchema<T>\n}\n\n/**\n * Creates graphql shield middleware making use of generics to type the definition of the rules.\n * @param schema A mapping of schema objects to the shield rules that define the access to that object\n * @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.\n *\n * Usage:\n * import { Resolvers } from '../generated/types'\n *\n * const isCoordinator = hasRoleRule(UserRoles.Coordinator)\n * const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)\n * const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)\n *\n * const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)\n * const isDataAuthor = or(isCoordinator, isSystemAdmin)\n *\n * const shieldSchema = createShieldSchema<Resolvers>({\n * Query: {\n * '*': isAuthorisedUser,\n * user: isCoordinator,\n * findUsers: isCoordinator,\n * },\n * Mutation: {\n * '*': isDataAuthor,\n * createUser: isCoordinator,\n * },\n * // specific rules for mutations, types, fields...\n * }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})\n *\n * const unprotected = makeExecutableSchema({...})\n * return applyMiddleware(unprotected, shieldSchema)\n */\nexport function createShieldSchema<TRootResolvers>(schema: ShieldSchema<TRootResolvers>, options?: ShieldOptions) {\n return shield(schema as IRules, {\n allowExternalErrors: true,\n ...options,\n })\n}\n\n/**\n * A shield rule tree typed against a resolver map (typically your generated `Resolvers` type).\n *\n * Mirrors the shape of `TResolver` so each type/field can be assigned a {@link ShieldRule}.\n * Object levels may also include a `'*'` key — a fallback rule applied to any field at that\n * level that doesn't have an explicit rule. Leaf (primitive) positions collapse to a single\n * {@link ShieldRule}.\n *\n * Usage:\n *\n * const schema: ShieldSchema<Resolvers> = {\n * Query: {\n * '*': isAuthorisedUser,\n * publicThing: allow,\n * },\n * Mutation: { createUser: isCoordinator },\n * }\n */\nexport type ShieldSchema<TResolver> = TResolver extends Primitive\n ? ShieldRule\n : {\n [key in keyof TResolver]?: ShieldSchema<TResolver[key]> | ShieldRule\n } & {\n '*'?: ShieldRule\n }\n\n/**\n * Shield rule that passes when the current user has the given role.\n * @param role The role the user must have on `ctx.user.roles`.\n * @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.\n */\nexport function hasRoleRule(role: string, ruleName?: string): ShieldRule {\n return rule(ruleName ?? `hasRole-${role}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.roles.includes(role) === true,\n )\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles.\n * Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRule(...roles: string[]): ShieldRule {\n return hasAnyRoleRuleWithName(`hasAnyRole-${roles.join(',')}`, ...roles)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.\n * Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.\n * @param ruleName The name (and cache key) for the rule.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRuleWithName(ruleName: string, ...roles: string[]): ShieldRule {\n return rule(ruleName, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => {\n return user?.roles.some((role: string) => roles.includes(role)) === true\n })\n}\n\n/**\n * Shield rule that passes when the current user has the given scope.\n * @param scope The scope the user must have on `ctx.user.scopes`.\n */\nexport function hasScopeRule(scope: string): ShieldRule {\n return rule(`hasScope-${scope}`, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => user?.scopes.includes(scope) === true)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given scopes.\n * @param scopes The set of scopes, any of which satisfies the rule.\n */\nexport function hasAnyScopeRule(...scopes: string[]): ShieldRule {\n return rule(`hasAnyScope-${scopes.join(',')}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.scopes.some((scope: string) => scopes.includes(scope)) === true,\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { GraphQLError } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a `GraphQLError` representing an authorization failure.
|
|
4
|
+
*
|
|
5
|
+
* Sets the Apollo-standard `FORBIDDEN` extension code and an HTTP 403 status, so clients and
|
|
6
|
+
* gateways that inspect `extensions.code` / `extensions.http.status` see a consistent shape.
|
|
7
|
+
*
|
|
8
|
+
* @param message Error message shown to the client. Defaults to `'Not Authorized!'`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const unauthorisedError: (message?: string) => GraphQLError;
|
|
11
|
+
/**
|
|
12
|
+
* Throws an {@link unauthorisedError}. Return type is `never` so TypeScript narrows control flow past the call.
|
|
13
|
+
* @param message Optional override for the error message.
|
|
14
|
+
*/
|
|
15
|
+
export declare function throwUnauthorised(message?: string): never;
|
|
16
|
+
/**
|
|
17
|
+
* Assertion helper: throws an {@link unauthorisedError} when `hasPermission` is false, otherwise narrows it to `true`.
|
|
18
|
+
*
|
|
19
|
+
* Use at the top of resolvers or services to fail fast on authorization checks while keeping
|
|
20
|
+
* the happy path unindented.
|
|
21
|
+
*
|
|
22
|
+
* @param hasPermission Boolean guard; the function returns normally only when this is `true`.
|
|
23
|
+
* @param message Optional override for the error message thrown on failure.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
*
|
|
27
|
+
* permissionInvariant(ctx.user?.id === record.ownerId, 'Only the owner can edit this record')
|
|
28
|
+
* // past this line, TypeScript knows the condition held
|
|
29
|
+
*/
|
|
30
|
+
export declare function permissionInvariant(hasPermission: boolean, message?: string): asserts hasPermission;
|
package/unauthorised.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var graphql = require('graphql');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a `GraphQLError` representing an authorization failure.
|
|
7
|
+
*
|
|
8
|
+
* Sets the Apollo-standard `FORBIDDEN` extension code and an HTTP 403 status, so clients and
|
|
9
|
+
* gateways that inspect `extensions.code` / `extensions.http.status` see a consistent shape.
|
|
10
|
+
*
|
|
11
|
+
* @param message Error message shown to the client. Defaults to `'Not Authorized!'`.
|
|
12
|
+
*/
|
|
13
|
+
const unauthorisedError = (message = 'Not Authorized!') => new graphql.GraphQLError(message, {
|
|
14
|
+
extensions: { code: 'FORBIDDEN', http: { status: 403 } },
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Throws an {@link unauthorisedError}. Return type is `never` so TypeScript narrows control flow past the call.
|
|
18
|
+
* @param message Optional override for the error message.
|
|
19
|
+
*/
|
|
20
|
+
function throwUnauthorised(message) {
|
|
21
|
+
throw unauthorisedError(message);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Assertion helper: throws an {@link unauthorisedError} when `hasPermission` is false, otherwise narrows it to `true`.
|
|
25
|
+
*
|
|
26
|
+
* Use at the top of resolvers or services to fail fast on authorization checks while keeping
|
|
27
|
+
* the happy path unindented.
|
|
28
|
+
*
|
|
29
|
+
* @param hasPermission Boolean guard; the function returns normally only when this is `true`.
|
|
30
|
+
* @param message Optional override for the error message thrown on failure.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
*
|
|
34
|
+
* permissionInvariant(ctx.user?.id === record.ownerId, 'Only the owner can edit this record')
|
|
35
|
+
* // past this line, TypeScript knows the condition held
|
|
36
|
+
*/
|
|
37
|
+
function permissionInvariant(hasPermission, message) {
|
|
38
|
+
if (!hasPermission)
|
|
39
|
+
throwUnauthorised(message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.permissionInvariant = permissionInvariant;
|
|
43
|
+
exports.throwUnauthorised = throwUnauthorised;
|
|
44
|
+
exports.unauthorisedError = unauthorisedError;
|
|
45
|
+
//# sourceMappingURL=unauthorised.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unauthorised.js","sources":["../src/unauthorised.ts"],"sourcesContent":["import { GraphQLError } from 'graphql'\n\n/**\n * Builds a `GraphQLError` representing an authorization failure.\n *\n * Sets the Apollo-standard `FORBIDDEN` extension code and an HTTP 403 status, so clients and\n * gateways that inspect `extensions.code` / `extensions.http.status` see a consistent shape.\n *\n * @param message Error message shown to the client. Defaults to `'Not Authorized!'`.\n */\nexport const unauthorisedError = (message: string = 'Not Authorized!') =>\n new GraphQLError(message, {\n extensions: { code: 'FORBIDDEN', http: { status: 403 } },\n })\n\n/**\n * Throws an {@link unauthorisedError}. Return type is `never` so TypeScript narrows control flow past the call.\n * @param message Optional override for the error message.\n */\nexport function throwUnauthorised(message?: string): never {\n throw unauthorisedError(message)\n}\n\n/**\n * Assertion helper: throws an {@link unauthorisedError} when `hasPermission` is false, otherwise narrows it to `true`.\n *\n * Use at the top of resolvers or services to fail fast on authorization checks while keeping\n * the happy path unindented.\n *\n * @param hasPermission Boolean guard; the function returns normally only when this is `true`.\n * @param message Optional override for the error message thrown on failure.\n *\n * Usage:\n *\n * permissionInvariant(ctx.user?.id === record.ownerId, 'Only the owner can edit this record')\n * // past this line, TypeScript knows the condition held\n */\nexport function permissionInvariant(hasPermission: boolean, message?: string): asserts hasPermission {\n if (!hasPermission) throwUnauthorised(message)\n}\n"],"names":["GraphQLError"],"mappings":";;;;AAEA;;;;;;;AAOG;AACI,MAAM,iBAAiB,GAAG,CAAC,OAAA,GAAkB,iBAAiB,KACnE,IAAIA,oBAAY,CAAC,OAAO,EAAE;AACxB,IAAA,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE;AACzD,CAAA;AAEH;;;AAGG;AACG,SAAU,iBAAiB,CAAC,OAAgB,EAAA;AAChD,IAAA,MAAM,iBAAiB,CAAC,OAAO,CAAC;AAClC;AAEA;;;;;;;;;;;;;AAaG;AACG,SAAU,mBAAmB,CAAC,aAAsB,EAAE,OAAgB,EAAA;AAC1E,IAAA,IAAI,CAAC,aAAa;QAAE,iBAAiB,CAAC,OAAO,CAAC;AAChD;;;;;;"}
|
package/unauthorised.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { GraphQLError } from 'graphql';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds a `GraphQLError` representing an authorization failure.
|
|
5
|
+
*
|
|
6
|
+
* Sets the Apollo-standard `FORBIDDEN` extension code and an HTTP 403 status, so clients and
|
|
7
|
+
* gateways that inspect `extensions.code` / `extensions.http.status` see a consistent shape.
|
|
8
|
+
*
|
|
9
|
+
* @param message Error message shown to the client. Defaults to `'Not Authorized!'`.
|
|
10
|
+
*/
|
|
11
|
+
const unauthorisedError = (message = 'Not Authorized!') => new GraphQLError(message, {
|
|
12
|
+
extensions: { code: 'FORBIDDEN', http: { status: 403 } },
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Throws an {@link unauthorisedError}. Return type is `never` so TypeScript narrows control flow past the call.
|
|
16
|
+
* @param message Optional override for the error message.
|
|
17
|
+
*/
|
|
18
|
+
function throwUnauthorised(message) {
|
|
19
|
+
throw unauthorisedError(message);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Assertion helper: throws an {@link unauthorisedError} when `hasPermission` is false, otherwise narrows it to `true`.
|
|
23
|
+
*
|
|
24
|
+
* Use at the top of resolvers or services to fail fast on authorization checks while keeping
|
|
25
|
+
* the happy path unindented.
|
|
26
|
+
*
|
|
27
|
+
* @param hasPermission Boolean guard; the function returns normally only when this is `true`.
|
|
28
|
+
* @param message Optional override for the error message thrown on failure.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
*
|
|
32
|
+
* permissionInvariant(ctx.user?.id === record.ownerId, 'Only the owner can edit this record')
|
|
33
|
+
* // past this line, TypeScript knows the condition held
|
|
34
|
+
*/
|
|
35
|
+
function permissionInvariant(hasPermission, message) {
|
|
36
|
+
if (!hasPermission)
|
|
37
|
+
throwUnauthorised(message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { permissionInvariant, throwUnauthorised, unauthorisedError };
|
|
41
|
+
//# sourceMappingURL=unauthorised.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unauthorised.mjs","sources":["../src/unauthorised.ts"],"sourcesContent":["import { GraphQLError } from 'graphql'\n\n/**\n * Builds a `GraphQLError` representing an authorization failure.\n *\n * Sets the Apollo-standard `FORBIDDEN` extension code and an HTTP 403 status, so clients and\n * gateways that inspect `extensions.code` / `extensions.http.status` see a consistent shape.\n *\n * @param message Error message shown to the client. Defaults to `'Not Authorized!'`.\n */\nexport const unauthorisedError = (message: string = 'Not Authorized!') =>\n new GraphQLError(message, {\n extensions: { code: 'FORBIDDEN', http: { status: 403 } },\n })\n\n/**\n * Throws an {@link unauthorisedError}. Return type is `never` so TypeScript narrows control flow past the call.\n * @param message Optional override for the error message.\n */\nexport function throwUnauthorised(message?: string): never {\n throw unauthorisedError(message)\n}\n\n/**\n * Assertion helper: throws an {@link unauthorisedError} when `hasPermission` is false, otherwise narrows it to `true`.\n *\n * Use at the top of resolvers or services to fail fast on authorization checks while keeping\n * the happy path unindented.\n *\n * @param hasPermission Boolean guard; the function returns normally only when this is `true`.\n * @param message Optional override for the error message thrown on failure.\n *\n * Usage:\n *\n * permissionInvariant(ctx.user?.id === record.ownerId, 'Only the owner can edit this record')\n * // past this line, TypeScript knows the condition held\n */\nexport function permissionInvariant(hasPermission: boolean, message?: string): asserts hasPermission {\n if (!hasPermission) throwUnauthorised(message)\n}\n"],"names":[],"mappings":";;AAEA;;;;;;;AAOG;AACI,MAAM,iBAAiB,GAAG,CAAC,OAAA,GAAkB,iBAAiB,KACnE,IAAI,YAAY,CAAC,OAAO,EAAE;AACxB,IAAA,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE;AACzD,CAAA;AAEH;;;AAGG;AACG,SAAU,iBAAiB,CAAC,OAAgB,EAAA;AAChD,IAAA,MAAM,iBAAiB,CAAC,OAAO,CAAC;AAClC;AAEA;;;;;;;;;;;;;AAaG;AACG,SAAU,mBAAmB,CAAC,aAAsB,EAAE,OAAgB,EAAA;AAC1E,IAAA,IAAI,CAAC,aAAa;QAAE,iBAAiB,CAAC,OAAO,CAAC;AAChD;;;;"}
|