@axinom/mosaic-id-guard 0.18.0-rc.9 → 0.18.0
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/dist/common/helpers/guard-authorization.d.ts +22 -0
- package/dist/common/helpers/guard-authorization.d.ts.map +1 -0
- package/dist/common/helpers/guard-authorization.js +49 -0
- package/dist/common/helpers/guard-authorization.js.map +1 -0
- package/dist/common/id-guard-error.d.ts +1 -0
- package/dist/common/id-guard-error.d.ts.map +1 -1
- package/dist/common/id-guard-error.js +7 -1
- package/dist/common/id-guard-error.js.map +1 -1
- package/dist/common/id-guard-errors.d.ts +8 -0
- package/dist/common/id-guard-errors.d.ts.map +1 -1
- package/dist/common/id-guard-errors.js +8 -0
- package/dist/common/id-guard-errors.js.map +1 -1
- package/dist/graphql/ax-guard-plugin.d.ts +23 -0
- package/dist/graphql/ax-guard-plugin.d.ts.map +1 -0
- package/dist/graphql/ax-guard-plugin.js +29 -0
- package/dist/graphql/ax-guard-plugin.js.map +1 -0
- package/dist/graphql/enforce-strict-permissions.plugin.d.ts +1 -1
- package/dist/graphql/enforce-strict-permissions.plugin.d.ts.map +1 -1
- package/dist/graphql/index.d.ts +3 -2
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js +3 -2
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/{guard-plugin.d.ts → query-mutation-guard-plugin.d.ts} +2 -2
- package/dist/graphql/query-mutation-guard-plugin.d.ts.map +1 -0
- package/dist/graphql/{guard-plugin.js → query-mutation-guard-plugin.js} +5 -26
- package/dist/graphql/query-mutation-guard-plugin.js.map +1 -0
- package/dist/graphql/subscription-guard-plugin.d.ts +20 -0
- package/dist/graphql/subscription-guard-plugin.d.ts.map +1 -0
- package/dist/graphql/subscription-guard-plugin.js +81 -0
- package/dist/graphql/subscription-guard-plugin.js.map +1 -0
- package/package.json +9 -10
- package/src/common/helpers/guard-authorization.ts +76 -0
- package/src/common/id-guard-error.ts +13 -0
- package/src/common/id-guard-errors.ts +10 -0
- package/src/graphql/ax-guard-plugin.ts +29 -0
- package/src/graphql/enforce-strict-permissions.plugin.ts +1 -1
- package/src/graphql/index.ts +3 -2
- package/src/graphql/{guard-plugin.spec.ts → query-mutation-guard-plugin.spec.ts} +3 -3
- package/src/graphql/{guard-plugin.ts → query-mutation-guard-plugin.ts} +12 -36
- package/src/graphql/subscription-guard-plugin.spec.ts +257 -0
- package/src/graphql/subscription-guard-plugin.ts +112 -0
- package/dist/graphql/guard-plugin.d.ts.map +0 -1
- package/dist/graphql/guard-plugin.js.map +0 -1
- package/dist/graphql/subscription-authorization-hook-factory.d.ts +0 -13
- package/dist/graphql/subscription-authorization-hook-factory.d.ts.map +0 -1
- package/dist/graphql/subscription-authorization-hook-factory.js +0 -182
- package/dist/graphql/subscription-authorization-hook-factory.js.map +0 -1
- package/src/graphql/subscription-authorization-hook-factory.spec.ts +0 -749
- package/src/graphql/subscription-authorization-hook-factory.ts +0 -286
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { makePluginByCombiningPlugins } from 'graphile-utils';
|
|
2
|
+
import { QueryMutationGuardPlugin } from './query-mutation-guard-plugin';
|
|
3
|
+
import { SubscriptionGuardPlugin } from './subscription-guard-plugin';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AxGuard plugin is created by combining two plugins - `QueryMutationGuardPlugin`
|
|
7
|
+
* and `SubscriptionGuardPlugin`.
|
|
8
|
+
*
|
|
9
|
+
* This plugin handles authorization for GraphQL resources exposed by the APIs.
|
|
10
|
+
* For Queries and Mutations an error is thrown if the authorization fails.
|
|
11
|
+
*
|
|
12
|
+
* For subscriptions, if the JWT token expires while subscription events are emitted,
|
|
13
|
+
* the websocket connection is closed with `4403` code, allowing the client to automatically
|
|
14
|
+
* re-establish the connection.
|
|
15
|
+
* For authorization errors, the websocket is closed with `4401` code.
|
|
16
|
+
*
|
|
17
|
+
* **!!!!!!!!!! IMPORTANT !!!!!!!!!!**
|
|
18
|
+
*
|
|
19
|
+
* When using this plugin with subscriptions, it's mandatory to send the reference to
|
|
20
|
+
* the websocket through Extended GraphQL Context with the key name `websocket`.
|
|
21
|
+
* `getWebsocketFromRequest` from `@axinom/mosaic-service-common` can be used to extract the
|
|
22
|
+
* websocket from request.
|
|
23
|
+
*
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
export const AxGuardPlugin = makePluginByCombiningPlugins(
|
|
27
|
+
QueryMutationGuardPlugin,
|
|
28
|
+
SubscriptionGuardPlugin,
|
|
29
|
+
);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
PermissionDefinition,
|
|
5
5
|
} from '@axinom/mosaic-id-utils';
|
|
6
6
|
import { Logger } from '@axinom/mosaic-service-common';
|
|
7
|
-
import { Plugin } from '
|
|
7
|
+
import { Plugin } from 'postgraphile';
|
|
8
8
|
import { removeProperties } from '../common/helpers';
|
|
9
9
|
|
|
10
10
|
interface PluginContext {
|
package/src/graphql/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from './ax-guard-plugin';
|
|
1
2
|
export * from './enforce-strict-permissions.plugin';
|
|
2
3
|
export * from './guard-context';
|
|
3
4
|
export {
|
|
@@ -8,5 +9,5 @@ export {
|
|
|
8
9
|
throwEndUserAuthError,
|
|
9
10
|
throwManagementAuthError,
|
|
10
11
|
} from './guard-middleware';
|
|
11
|
-
export * from './guard-plugin';
|
|
12
|
-
export * from './subscription-
|
|
12
|
+
export * from './query-mutation-guard-plugin';
|
|
13
|
+
export * from './subscription-guard-plugin';
|
|
@@ -11,7 +11,7 @@ import { gql, makeExtendSchemaPlugin } from 'graphile-utils';
|
|
|
11
11
|
import { graphql, GraphQLSchema, parse, subscribe } from 'graphql';
|
|
12
12
|
import { PubSub } from 'graphql-subscriptions';
|
|
13
13
|
import { IdGuardErrors, SubjectType } from '../common';
|
|
14
|
-
import {
|
|
14
|
+
import { QueryMutationGuardPlugin } from './query-mutation-guard-plugin';
|
|
15
15
|
|
|
16
16
|
const pubSub = new PubSub();
|
|
17
17
|
|
|
@@ -63,7 +63,7 @@ const makeSchemaWithSpyAndPlugins = async (
|
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
})),
|
|
66
|
-
|
|
66
|
+
QueryMutationGuardPlugin,
|
|
67
67
|
],
|
|
68
68
|
{
|
|
69
69
|
optionKey: 'optionValue',
|
|
@@ -77,7 +77,7 @@ const makeEchoSpy = () =>
|
|
|
77
77
|
return args.message;
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
describe('
|
|
80
|
+
describe('QueryMutationGuardPlugin', () => {
|
|
81
81
|
it('When neither a PermissionDefinition object nor EndUserAuthorizationConfig object is present in Postgraphile build options, an error is raised', async () => {
|
|
82
82
|
// Arrange
|
|
83
83
|
const spy = makeEchoSpy();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { makeWrapResolversPlugin } from 'graphile-utils';
|
|
2
2
|
import {
|
|
3
|
-
assertGenericAuthenticatedSubject,
|
|
4
3
|
EndUserAuthenticationContext,
|
|
5
4
|
ManagementAuthenticationContext,
|
|
6
5
|
} from '../common';
|
|
@@ -8,8 +7,7 @@ import {
|
|
|
8
7
|
AxGuardOptions,
|
|
9
8
|
validatePostgraphileBuildOptions,
|
|
10
9
|
} from '../common/guard-utils';
|
|
11
|
-
import {
|
|
12
|
-
import { handleManagementUserAuthorization } from '../common/handle-management-user-authorization';
|
|
10
|
+
import { handleAuthorization } from '../common/helpers/guard-authorization';
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* Postgraphile plugin that wraps resolver execution into an authentication check, making sure that that JwtToken subject is authorized to access a GraphQL resource.
|
|
@@ -20,7 +18,7 @@ import { handleManagementUserAuthorization } from '../common/handle-management-u
|
|
|
20
18
|
* @param additionalGraphQLContextFromRequest should be of type `Record<string, any> & { subject: AuthenticatedSubject, authErrorInfo: AxGuardErrorInfo }`
|
|
21
19
|
* @param graphileBuildOptions should be of type `Partial<Options> & { permissionDefinition: PermissionDefinition, serviceId: string, ensureOnlyAuthentication: boolean }`
|
|
22
20
|
*/
|
|
23
|
-
export const
|
|
21
|
+
export const QueryMutationGuardPlugin = makeWrapResolversPlugin(
|
|
24
22
|
(
|
|
25
23
|
{ scope },
|
|
26
24
|
_build,
|
|
@@ -66,37 +64,15 @@ export const AxGuardPlugin = makeWrapResolversPlugin(
|
|
|
66
64
|
// Validate if the Postgraphile build options related to authorization are correctly set.
|
|
67
65
|
validatePostgraphileBuildOptions(options);
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
authErrorInfo,
|
|
80
|
-
);
|
|
81
|
-
return resolver(source, args, context, resolveInfo);
|
|
82
|
-
} else {
|
|
83
|
-
/**
|
|
84
|
-
* This block handles end-user operations.
|
|
85
|
-
* If the if condition (options.authorizationOptions.permissionDefinition !== undefined) falls through to this else branch,
|
|
86
|
-
* it is assumed that the request is a end-user call.
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
if (subject !== undefined && subject !== null) {
|
|
90
|
-
assertGenericAuthenticatedSubject(subject);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
handleEndUserAuthorization(
|
|
94
|
-
options.authorizationOptions,
|
|
95
|
-
options.ensureOnlyAuthentication,
|
|
96
|
-
subject,
|
|
97
|
-
authErrorInfo,
|
|
98
|
-
);
|
|
99
|
-
return resolver(source, args, context, resolveInfo);
|
|
100
|
-
}
|
|
67
|
+
return handleAuthorization(
|
|
68
|
+
subject,
|
|
69
|
+
authErrorInfo,
|
|
70
|
+
options,
|
|
71
|
+
resolver,
|
|
72
|
+
source,
|
|
73
|
+
args,
|
|
74
|
+
context,
|
|
75
|
+
resolveInfo,
|
|
76
|
+
);
|
|
101
77
|
},
|
|
102
78
|
);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSchema,
|
|
3
|
+
QueryPlugin,
|
|
4
|
+
StandardTypesPlugin,
|
|
5
|
+
SubscriptionPlugin,
|
|
6
|
+
} from 'graphile-build';
|
|
7
|
+
import { gql, makeExtendSchemaPlugin } from 'graphile-utils';
|
|
8
|
+
import { GraphQLSchema, parse, subscribe } from 'graphql';
|
|
9
|
+
import { PubSub } from 'graphql-subscriptions';
|
|
10
|
+
import { CloseCode } from 'graphql-ws';
|
|
11
|
+
import { IdGuardErrors, SubjectType } from '../common';
|
|
12
|
+
import { SubscriptionGuardPlugin } from './subscription-guard-plugin';
|
|
13
|
+
|
|
14
|
+
const pubSub = new PubSub();
|
|
15
|
+
|
|
16
|
+
const makeSchemaWithSpyAndPlugins = async (
|
|
17
|
+
spy: any,
|
|
18
|
+
options = {},
|
|
19
|
+
): Promise<GraphQLSchema> =>
|
|
20
|
+
buildSchema(
|
|
21
|
+
[
|
|
22
|
+
StandardTypesPlugin,
|
|
23
|
+
QueryPlugin,
|
|
24
|
+
SubscriptionPlugin,
|
|
25
|
+
makeExtendSchemaPlugin(() => ({
|
|
26
|
+
typeDefs: gql`
|
|
27
|
+
extend type Query {
|
|
28
|
+
echo(message: String!): String
|
|
29
|
+
}
|
|
30
|
+
extend type Subscription {
|
|
31
|
+
echoSubscription: String
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
resolvers: {
|
|
35
|
+
Query: {
|
|
36
|
+
echo: spy,
|
|
37
|
+
},
|
|
38
|
+
Subscription: {
|
|
39
|
+
echoSubscription: {
|
|
40
|
+
subscribe: () => pubSub.asyncIterator('echoSubscription'),
|
|
41
|
+
resolve: () => {
|
|
42
|
+
return 'Hello Subscription';
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
})),
|
|
48
|
+
SubscriptionGuardPlugin,
|
|
49
|
+
],
|
|
50
|
+
{
|
|
51
|
+
optionKey: 'optionValue',
|
|
52
|
+
serviceId: 'test-service',
|
|
53
|
+
...options,
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const makeEchoSpy = () =>
|
|
58
|
+
jest.fn((parent, args) => {
|
|
59
|
+
return args.message;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('SubscriptionGuardPlugin', () => {
|
|
63
|
+
it('When a websocket reference is not present in the GraphQL Context, a WebSocketNotFound error is thrown', async () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const spy = makeEchoSpy();
|
|
66
|
+
|
|
67
|
+
const schema = await makeSchemaWithSpyAndPlugins(spy);
|
|
68
|
+
|
|
69
|
+
const document = parse('subscription { echoSubscription }');
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
// Create subscription
|
|
73
|
+
const subscription: any = await subscribe({
|
|
74
|
+
schema,
|
|
75
|
+
document,
|
|
76
|
+
rootValue: null,
|
|
77
|
+
contextValue: {},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
expect(subscription.errors).toBeDefined();
|
|
82
|
+
expect(subscription.errors[0].message).toBe(
|
|
83
|
+
IdGuardErrors.WebSocketNotFound.message,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('When a the token is expired, the websocket is closed with code Forbidden (4403)', async () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const spy = makeEchoSpy();
|
|
90
|
+
|
|
91
|
+
const schema = await makeSchemaWithSpyAndPlugins(spy, {
|
|
92
|
+
serviceId: 'test-service',
|
|
93
|
+
permissionDefinition: {
|
|
94
|
+
gqlOptions: {
|
|
95
|
+
anonymousGqlOperations: [],
|
|
96
|
+
ignoredGqlOperations: [],
|
|
97
|
+
},
|
|
98
|
+
permissions: [
|
|
99
|
+
{
|
|
100
|
+
key: 'EDITOR',
|
|
101
|
+
title: 'EDITOR',
|
|
102
|
+
gqlOperations: ['otherEndpoint', 'echoSubscription'],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const document = parse('subscription { echoSubscription }');
|
|
109
|
+
|
|
110
|
+
const websocketCloseSpy = jest.fn();
|
|
111
|
+
|
|
112
|
+
// Act
|
|
113
|
+
// Create subscription
|
|
114
|
+
const subscription: any = await subscribe({
|
|
115
|
+
schema,
|
|
116
|
+
document,
|
|
117
|
+
rootValue: null,
|
|
118
|
+
contextValue: {
|
|
119
|
+
websocket: {
|
|
120
|
+
close: websocketCloseSpy,
|
|
121
|
+
},
|
|
122
|
+
subject: {
|
|
123
|
+
sub: 'test-user',
|
|
124
|
+
permissions: { 'test-service': ['EDITOR'] },
|
|
125
|
+
exp: new Date().getTime() / 1000 - 20, // set expiry time of the token to currentTime - 20 seconds
|
|
126
|
+
subjectType: SubjectType.UserAccount,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
expect(subscription.errors).toBeDefined();
|
|
133
|
+
expect(subscription.errors[0].message).toBe(
|
|
134
|
+
IdGuardErrors.AccessTokenExpired.message,
|
|
135
|
+
);
|
|
136
|
+
expect(websocketCloseSpy).toHaveBeenCalledWith(
|
|
137
|
+
CloseCode.Forbidden,
|
|
138
|
+
IdGuardErrors.AccessTokenExpired.code,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('When a the user is not authorized to invoke the subscription, the websocket is closed with code Unauthorized (4401)', async () => {
|
|
143
|
+
// Arrange
|
|
144
|
+
const spy = makeEchoSpy();
|
|
145
|
+
|
|
146
|
+
const schema = await makeSchemaWithSpyAndPlugins(spy, {
|
|
147
|
+
serviceId: 'test-service',
|
|
148
|
+
permissionDefinition: {
|
|
149
|
+
gqlOptions: {
|
|
150
|
+
anonymousGqlOperations: [],
|
|
151
|
+
ignoredGqlOperations: [],
|
|
152
|
+
},
|
|
153
|
+
permissions: [
|
|
154
|
+
{
|
|
155
|
+
key: 'EDITOR',
|
|
156
|
+
title: 'EDITOR',
|
|
157
|
+
gqlOperations: ['otherEndpoint', 'echoSubscription'],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const document = parse('subscription { echoSubscription }');
|
|
164
|
+
|
|
165
|
+
const websocketCloseSpy = jest.fn();
|
|
166
|
+
|
|
167
|
+
// Act
|
|
168
|
+
// Create subscription
|
|
169
|
+
const subscription: any = await subscribe({
|
|
170
|
+
schema,
|
|
171
|
+
document,
|
|
172
|
+
rootValue: null,
|
|
173
|
+
contextValue: {
|
|
174
|
+
websocket: {
|
|
175
|
+
close: websocketCloseSpy,
|
|
176
|
+
},
|
|
177
|
+
subject: {
|
|
178
|
+
sub: 'test-user',
|
|
179
|
+
permissions: { 'test-service': ['READER'] },
|
|
180
|
+
exp: new Date().getTime() / 1000 + 20, // set expiry time of the token to currentTime + 20 seconds
|
|
181
|
+
subjectType: SubjectType.UserAccount,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Assert
|
|
187
|
+
expect(subscription.errors).toBeDefined();
|
|
188
|
+
expect(subscription.errors[0].message).toBe(
|
|
189
|
+
IdGuardErrors.UserNotAuthorized.message,
|
|
190
|
+
);
|
|
191
|
+
expect(websocketCloseSpy).toHaveBeenCalledWith(
|
|
192
|
+
CloseCode.Unauthorized,
|
|
193
|
+
IdGuardErrors.UserNotAuthorized.message,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('When the user is authorized to invoke the subscription with non-expired token, the subscription event payload is returned', async () => {
|
|
198
|
+
// Arrange
|
|
199
|
+
const spy = makeEchoSpy();
|
|
200
|
+
|
|
201
|
+
const schema = await makeSchemaWithSpyAndPlugins(spy, {
|
|
202
|
+
serviceId: 'test-service',
|
|
203
|
+
permissionDefinition: {
|
|
204
|
+
gqlOptions: {
|
|
205
|
+
anonymousGqlOperations: [],
|
|
206
|
+
ignoredGqlOperations: [],
|
|
207
|
+
},
|
|
208
|
+
permissions: [
|
|
209
|
+
{
|
|
210
|
+
key: 'EDITOR',
|
|
211
|
+
title: 'EDITOR',
|
|
212
|
+
gqlOperations: ['otherEndpoint', 'echoSubscription'],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const document = parse('subscription { echoSubscription }');
|
|
219
|
+
|
|
220
|
+
const websocketCloseSpy = jest.fn();
|
|
221
|
+
|
|
222
|
+
// Act
|
|
223
|
+
// Create subscription
|
|
224
|
+
const subscription: any = await subscribe({
|
|
225
|
+
schema,
|
|
226
|
+
document,
|
|
227
|
+
rootValue: null,
|
|
228
|
+
contextValue: {
|
|
229
|
+
websocket: {
|
|
230
|
+
close: websocketCloseSpy,
|
|
231
|
+
},
|
|
232
|
+
subject: {
|
|
233
|
+
sub: 'test-user',
|
|
234
|
+
permissions: { 'test-service': ['EDITOR'] },
|
|
235
|
+
exp: new Date().getTime() / 1000 + 20, // set expiry time of the token to currentTime + 20 seconds
|
|
236
|
+
subjectType: SubjectType.UserAccount,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const resolveSubscription = subscription.next();
|
|
242
|
+
|
|
243
|
+
// Publish trigger to invoke subscription resolver
|
|
244
|
+
pubSub.publish('echoSubscription', 'message');
|
|
245
|
+
|
|
246
|
+
// Await for the subscription resolver to respond
|
|
247
|
+
const result = await resolveSubscription;
|
|
248
|
+
|
|
249
|
+
// Assert
|
|
250
|
+
expect(subscription.errors).toBeUndefined();
|
|
251
|
+
expect(websocketCloseSpy).not.toHaveBeenCalled();
|
|
252
|
+
expect(result.value.data.echoSubscription).toBe('Hello Subscription');
|
|
253
|
+
|
|
254
|
+
// Close subscription
|
|
255
|
+
await subscription.return();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { MosaicError } from '@axinom/mosaic-service-common';
|
|
2
|
+
import { CloseCode } from 'graphql-ws';
|
|
3
|
+
import { Plugin } from 'postgraphile';
|
|
4
|
+
import {
|
|
5
|
+
assertIdGuardError,
|
|
6
|
+
EndUserAuthenticationContext,
|
|
7
|
+
IdGuardErrors,
|
|
8
|
+
ManagementAuthenticationContext,
|
|
9
|
+
} from '../common';
|
|
10
|
+
import {
|
|
11
|
+
AxGuardOptions,
|
|
12
|
+
validatePostgraphileBuildOptions,
|
|
13
|
+
} from '../common/guard-utils';
|
|
14
|
+
import { handleAuthorization } from '../common/helpers/guard-authorization';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This plugin wraps around the `subscribe` method for subscriptions
|
|
18
|
+
* and performs authorization, making sure that that JwtToken subject is authorized to access a GraphQL resource.
|
|
19
|
+
*
|
|
20
|
+
* The websocket is closed with error code 4403 if the access token has expired, allowing the client
|
|
21
|
+
* to re-establish the connection.
|
|
22
|
+
* If any other error is thrown, the websocket is closed with error code 4401.
|
|
23
|
+
*
|
|
24
|
+
* **!!!!!!!!!! IMPORTANT !!!!!!!!!!**
|
|
25
|
+
*
|
|
26
|
+
* When using this plugin, it's mandatory to send the reference to the websocket through
|
|
27
|
+
* Extended GraphQL Context with the key name `websocket`.
|
|
28
|
+
* `getWebsocketFromRequest` from `@axinom/mosaic-service-common` can be used to extract the
|
|
29
|
+
* websocket from request.
|
|
30
|
+
*
|
|
31
|
+
* @param builder
|
|
32
|
+
*/
|
|
33
|
+
export const SubscriptionGuardPlugin: Plugin = (builder) => {
|
|
34
|
+
builder.hook(
|
|
35
|
+
'GraphQLObjectType:fields:field',
|
|
36
|
+
(field, build, _context) => {
|
|
37
|
+
if (field.subscribe) {
|
|
38
|
+
const oldSubscribe = field.subscribe;
|
|
39
|
+
field.subscribe = async function (
|
|
40
|
+
parent: unknown,
|
|
41
|
+
args,
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
context: any,
|
|
44
|
+
info,
|
|
45
|
+
) {
|
|
46
|
+
if (!context.websocket) {
|
|
47
|
+
throw new MosaicError(IdGuardErrors.WebSocketNotFound);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const options: AxGuardOptions = {
|
|
51
|
+
authorizationOptions: {
|
|
52
|
+
endUserAuthorizationConfig:
|
|
53
|
+
build.options.endUserAuthorizationConfig,
|
|
54
|
+
operation: info.fieldName,
|
|
55
|
+
permissionDefinition: build.options.permissionDefinition,
|
|
56
|
+
serviceId: build.options.serviceId,
|
|
57
|
+
},
|
|
58
|
+
ensureOnlyAuthentication:
|
|
59
|
+
build.options.ensureOnlyAuthentication ?? false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const { authErrorInfo, subject } = context as
|
|
63
|
+
| ManagementAuthenticationContext
|
|
64
|
+
| EndUserAuthenticationContext; // The context could be either a Management or an End-User context.
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Validate if the Postgraphile build options related to authorization are correctly set.
|
|
68
|
+
validatePostgraphileBuildOptions(options);
|
|
69
|
+
|
|
70
|
+
return handleAuthorization(
|
|
71
|
+
subject,
|
|
72
|
+
authErrorInfo,
|
|
73
|
+
options,
|
|
74
|
+
oldSubscribe,
|
|
75
|
+
parent,
|
|
76
|
+
args,
|
|
77
|
+
context,
|
|
78
|
+
info,
|
|
79
|
+
);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
assertIdGuardError(error);
|
|
82
|
+
/**
|
|
83
|
+
* If the token has expired, we close the connection with CloseCode.Forbidden, and allow the client to automatically re-establish
|
|
84
|
+
* the connection with a new token.
|
|
85
|
+
*/
|
|
86
|
+
if (error.code === IdGuardErrors.AccessTokenExpired.code) {
|
|
87
|
+
/**
|
|
88
|
+
* `CloseCode.Forbidden` is used so that the client will automatically start retry process with a newer token.
|
|
89
|
+
* Any other CloseCode will result in a client exception and the client will not automatically retry.
|
|
90
|
+
*/
|
|
91
|
+
context.websocket.close(
|
|
92
|
+
CloseCode.Forbidden,
|
|
93
|
+
IdGuardErrors.AccessTokenExpired.code,
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
// Close the websocket connection.
|
|
97
|
+
context.websocket.close(CloseCode.Unauthorized, error.message);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return field;
|
|
104
|
+
},
|
|
105
|
+
[],
|
|
106
|
+
[],
|
|
107
|
+
// Add the `subscribe` method to the graphql fields, wrapping around the GQL resolver.
|
|
108
|
+
// It looks for the `@pgSubscription` directive, and adds the `subscribe` method.
|
|
109
|
+
// https://github.com/graphile/graphile-engine/blob/1bc8cfefdab7a61fd7ad287bcdff66298352e308/packages/pg-pubsub/src/PgSubscriptionResolverPlugin.ts
|
|
110
|
+
['PgSubscriptionResolver'],
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guard-plugin.d.ts","sourceRoot":"","sources":["../../src/graphql/guard-plugin.ts"],"names":[],"mappings":"AAaA;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,iCA+EzB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guard-plugin.js","sourceRoot":"","sources":["../../src/graphql/guard-plugin.ts"],"names":[],"mappings":";;;AAAA,mDAAyD;AACzD,sCAImB;AACnB,uDAG+B;AAC/B,2FAAqF;AACrF,yGAAmG;AAEnG;;;;;;;;GAQG;AACU,QAAA,aAAa,GAAG,IAAA,wCAAuB,EAClD,CACE,EAAE,KAAK,EAAE,EACT,MAAM,EACN,OAAO,EACP,EACE,wBAAwB,EACxB,SAAS,EACT,oBAAoB,EACpB,0BAA0B,GAC3B,EACD,EAAE;IACF,MAAM,uBAAuB,GAC3B,KAAK,CAAC,SAAS,KAAK,OAAO;QAC3B,CAAC,KAAK,CAAC,WAAW;YAChB,KAAK,CAAC,cAAc;YACpB,KAAK,CAAC,wBAAwB;YAC9B,KAAK,CAAC,yBAAyB;YAC/B,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAE9B,IAAI,uBAAuB,EAAE;QAC3B,OAAO;YACL,wBAAwB,EAAE,wBAAwB,aAAxB,wBAAwB,cAAxB,wBAAwB,GAAI,KAAK;YAC3D,oBAAoB,EAAE;gBACpB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,SAAS;gBACT,oBAAoB;gBACpB,0BAA0B;aAC3B;SACF,CAAC;KACH;IAED,OAAO,IAAI,CAAC;AACd,CAAC,EACD,CAAC,OAAuB,EAAE,EAAE,CAC1B,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE;IACrD,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,OAEH,CAAC,CAAC,mEAAmE;IAErG,sIAAsI;IACtI,OAAO,CAAC,oBAAoB,CAAC,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC;IAE/D,yFAAyF;IACzF,IAAA,8CAAgC,EAAC,OAAO,CAAC,CAAC;IAE1C,mHAAmH;IACnH,IAAI,OAAO,CAAC,oBAAoB,CAAC,oBAAoB,KAAK,SAAS,EAAE;QACnE,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI,EAAE;YAC7C,IAAA,0CAAiC,EAAC,OAAO,CAAC,CAAC;SAC5C;QAED,IAAA,wEAAiC,EAC/B,OAAO,CAAC,oBAAoB,EAC5B,OAAO,CAAC,wBAAwB,EAChC,OAAO,EACP,aAAa,CACd,CAAC;QACF,OAAO,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;KACrD;SAAM;QACL;;;;WAIG;QAEH,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI,EAAE;YAC7C,IAAA,0CAAiC,EAAC,OAAO,CAAC,CAAC;SAC5C;QAED,IAAA,0DAA0B,EACxB,OAAO,CAAC,oBAAoB,EAC5B,OAAO,CAAC,wBAAwB,EAChC,OAAO,EACP,aAAa,CACd,CAAC;QACF,OAAO,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;KACrD;AACH,CAAC,CACJ,CAAC"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Request, Response } from 'express';
|
|
2
|
-
import { PostGraphileOptions, PostGraphilePlugin } from 'postgraphile';
|
|
3
|
-
/**
|
|
4
|
-
* This is a hook plugin generator function that will use the PermissionDefinition object in PostGraphileOptions
|
|
5
|
-
* to authorize the Subscription operation done over a web socket.
|
|
6
|
-
* It checks if the authenticated user which initiates the subscription has required permissions.
|
|
7
|
-
* The actual plugin is built using this function when the build() function is called in PostgraphileOptionsBuilder.
|
|
8
|
-
* This function reference can be passed into addHookPluginGenerator() to activate authorization of subscriptions.
|
|
9
|
-
* @param PostGraphileOptions object containing permissions.
|
|
10
|
-
* @returns PostGraphilePlugin
|
|
11
|
-
*/
|
|
12
|
-
export declare const subscriptionAuthorizationHookFactory: (buildOptions: PostGraphileOptions<Request, Response> | undefined) => PostGraphilePlugin;
|
|
13
|
-
//# sourceMappingURL=subscription-authorization-hook-factory.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"subscription-authorization-hook-factory.d.ts","sourceRoot":"","sources":["../../src/graphql/subscription-authorization-hook-factory.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAU5C,OAAO,EAEL,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAgBtB;;;;;;;;GAQG;AACH,eAAO,MAAM,oCAAoC,iBACjC,oBAAoB,OAAO,EAAE,QAAQ,CAAC,GAAG,SAAS,KAC/D,kBA4MF,CAAC"}
|