@simtlix/simfinity-js 2.4.0 → 2.4.1
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 +53 -38
- package/package.json +1 -1
- package/src/auth/index.js +110 -31
- package/src/plugins.js +5 -2
package/README.md
CHANGED
|
@@ -20,12 +20,13 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
|
|
|
20
20
|
- [Adding Middlewares](#adding-middlewares)
|
|
21
21
|
- [Middleware Parameters](#middleware-parameters)
|
|
22
22
|
- [Common Use Cases](#common-use-cases)
|
|
23
|
-
- [Authorization
|
|
23
|
+
- [Authorization](#-authorization)
|
|
24
24
|
- [Quick Start](#quick-start-1)
|
|
25
25
|
- [Permission Schema](#permission-schema)
|
|
26
26
|
- [Rule Helpers](#rule-helpers)
|
|
27
27
|
- [Policy Expressions (JSON AST)](#policy-expressions-json-ast)
|
|
28
|
-
- [Integration with
|
|
28
|
+
- [Integration with GraphQL Yoga / Envelop](#integration-with-graphql-yoga--envelop)
|
|
29
|
+
- [Legacy: Integration with graphql-middleware](#legacy-integration-with-graphql-middleware)
|
|
29
30
|
- [Relationships](#-relationships)
|
|
30
31
|
- [Defining Relationships](#defining-relationships)
|
|
31
32
|
- [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
|
|
@@ -75,7 +76,7 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
|
|
|
75
76
|
- **Lifecycle Hooks**: Controller methods for granular control over operations
|
|
76
77
|
- **Custom Validation**: Field-level and type-level custom validations
|
|
77
78
|
- **Relationship Management**: Support for embedded and referenced relationships
|
|
78
|
-
- **Authorization
|
|
79
|
+
- **Authorization**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support
|
|
79
80
|
|
|
80
81
|
## 📦 Installation
|
|
81
82
|
|
|
@@ -629,17 +630,17 @@ simfinity.use((params, next) => {
|
|
|
629
630
|
5. **Performance consideration**: Middlewares run on every operation, keep them lightweight
|
|
630
631
|
6. **Use context wisely**: Store request-specific data in the GraphQL context object
|
|
631
632
|
|
|
632
|
-
## 🔐 Authorization
|
|
633
|
+
## 🔐 Authorization
|
|
633
634
|
|
|
634
|
-
Simfinity.js provides
|
|
635
|
+
Simfinity.js provides production-grade centralized GraphQL authorization supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies. It ships as a native Envelop plugin for GraphQL Yoga (recommended) and also supports the legacy graphql-middleware approach.
|
|
635
636
|
|
|
636
637
|
### Quick Start
|
|
637
638
|
|
|
638
639
|
```javascript
|
|
639
640
|
const { auth } = require('@simtlix/simfinity-js');
|
|
640
|
-
const {
|
|
641
|
+
const { createYoga } = require('graphql-yoga');
|
|
641
642
|
|
|
642
|
-
const {
|
|
643
|
+
const { createAuthPlugin, requireAuth, requireRole } = auth;
|
|
643
644
|
|
|
644
645
|
// Define your permission schema
|
|
645
646
|
const permissions = {
|
|
@@ -665,9 +666,9 @@ const permissions = {
|
|
|
665
666
|
},
|
|
666
667
|
};
|
|
667
668
|
|
|
668
|
-
// Create and
|
|
669
|
-
const
|
|
670
|
-
const
|
|
669
|
+
// Create the Envelop auth plugin and pass it to your server
|
|
670
|
+
const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
|
|
671
|
+
const yoga = createYoga({ schema, plugins: [authPlugin] });
|
|
671
672
|
```
|
|
672
673
|
|
|
673
674
|
### Permission Schema
|
|
@@ -869,25 +870,24 @@ Use `{ ref: 'path' }` to reference values:
|
|
|
869
870
|
- Unknown operators fail closed (deny)
|
|
870
871
|
- No `eval()` or `Function()` - pure object traversal
|
|
871
872
|
|
|
872
|
-
### Integration with
|
|
873
|
+
### Integration with GraphQL Yoga / Envelop
|
|
873
874
|
|
|
874
|
-
The auth
|
|
875
|
+
The recommended way to use the auth system is via the Envelop plugin, which works natively with GraphQL Yoga and any Envelop-based server. The plugin wraps resolvers in-place without rebuilding the schema, avoiding compatibility issues.
|
|
875
876
|
|
|
876
877
|
```javascript
|
|
877
|
-
const
|
|
878
|
-
const {
|
|
879
|
-
const { applyMiddleware } = require('graphql-middleware');
|
|
878
|
+
const { createYoga } = require('graphql-yoga');
|
|
879
|
+
const { createServer } = require('http');
|
|
880
880
|
const simfinity = require('@simtlix/simfinity-js');
|
|
881
881
|
|
|
882
882
|
const { auth } = simfinity;
|
|
883
|
-
const {
|
|
883
|
+
const { createAuthPlugin, requireAuth, requireRole, requirePermission } = auth;
|
|
884
884
|
|
|
885
885
|
// Define your types and connect them
|
|
886
886
|
simfinity.connect(null, UserType, 'user', 'users');
|
|
887
887
|
simfinity.connect(null, PostType, 'post', 'posts');
|
|
888
888
|
|
|
889
889
|
// Create base schema
|
|
890
|
-
const
|
|
890
|
+
const schema = simfinity.createSchema();
|
|
891
891
|
|
|
892
892
|
// Define permissions
|
|
893
893
|
const permissions = {
|
|
@@ -921,36 +921,51 @@ const permissions = {
|
|
|
921
921
|
},
|
|
922
922
|
};
|
|
923
923
|
|
|
924
|
-
// Create auth
|
|
925
|
-
const
|
|
926
|
-
defaultPolicy: 'DENY',
|
|
927
|
-
debug: false,
|
|
924
|
+
// Create auth plugin
|
|
925
|
+
const authPlugin = createAuthPlugin(permissions, {
|
|
926
|
+
defaultPolicy: 'DENY',
|
|
927
|
+
debug: false,
|
|
928
928
|
});
|
|
929
929
|
|
|
930
|
-
//
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
// Setup Express with context
|
|
934
|
-
const app = express();
|
|
935
|
-
|
|
936
|
-
app.use('/graphql', graphqlHTTP((req) => ({
|
|
930
|
+
// Setup Yoga with the auth plugin
|
|
931
|
+
const yoga = createYoga({
|
|
937
932
|
schema,
|
|
938
|
-
|
|
939
|
-
context: {
|
|
940
|
-
user: req.user, // Set by your authentication
|
|
941
|
-
},
|
|
942
|
-
formatError: simfinity.buildErrorFormatter((err) => {
|
|
943
|
-
console.error(err);
|
|
933
|
+
plugins: [authPlugin],
|
|
934
|
+
context: (req) => ({
|
|
935
|
+
user: req.user, // Set by your authentication layer
|
|
944
936
|
}),
|
|
945
|
-
})
|
|
937
|
+
});
|
|
946
938
|
|
|
947
|
-
|
|
939
|
+
const server = createServer(yoga);
|
|
940
|
+
server.listen(4000);
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### Legacy: Integration with graphql-middleware
|
|
944
|
+
|
|
945
|
+
> **Deprecated:** `applyMiddleware` from `graphql-middleware` rebuilds the schema via `mapSchema`,
|
|
946
|
+
> which can cause `"Schema must contain uniquely named types"` errors with Simfinity schemas.
|
|
947
|
+
> Use `createAuthPlugin` with GraphQL Yoga / Envelop instead.
|
|
948
|
+
|
|
949
|
+
```javascript
|
|
950
|
+
const { applyMiddleware } = require('graphql-middleware');
|
|
951
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
952
|
+
|
|
953
|
+
const { auth } = simfinity;
|
|
954
|
+
const { createAuthMiddleware, requireAuth, requireRole } = auth;
|
|
955
|
+
|
|
956
|
+
const baseSchema = simfinity.createSchema();
|
|
957
|
+
|
|
958
|
+
const authMiddleware = createAuthMiddleware(permissions, {
|
|
959
|
+
defaultPolicy: 'DENY',
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const schema = applyMiddleware(baseSchema, authMiddleware);
|
|
948
963
|
```
|
|
949
964
|
|
|
950
|
-
### Middleware Options
|
|
965
|
+
### Plugin / Middleware Options
|
|
951
966
|
|
|
952
967
|
```javascript
|
|
953
|
-
const
|
|
968
|
+
const plugin = createAuthPlugin(permissions, {
|
|
954
969
|
defaultPolicy: 'DENY', // 'ALLOW' or 'DENY' (default: 'DENY')
|
|
955
970
|
debug: false, // Enable debug logging
|
|
956
971
|
});
|
package/package.json
CHANGED
package/src/auth/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Simfinity GraphQL Authorization
|
|
2
|
+
* Simfinity GraphQL Authorization
|
|
3
3
|
*
|
|
4
4
|
* Production-grade centralized GraphQL authorization supporting:
|
|
5
5
|
* - RBAC / ABAC
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* import { auth } from '@simtlix/simfinity-js';
|
|
13
|
-
* import {
|
|
13
|
+
* import { createYoga } from 'graphql-yoga';
|
|
14
14
|
*
|
|
15
|
-
* const {
|
|
15
|
+
* const { createAuthPlugin, requireAuth, requireRole } = auth;
|
|
16
16
|
*
|
|
17
17
|
* const permissions = {
|
|
18
18
|
* Query: {
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
* }
|
|
29
29
|
* };
|
|
30
30
|
*
|
|
31
|
-
* const
|
|
32
|
-
* const
|
|
31
|
+
* const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
|
|
32
|
+
* const yoga = createYoga({ schema, plugins: [authPlugin] });
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import { GraphQLObjectType, defaultFieldResolver } from 'graphql';
|
|
35
36
|
import { UnauthenticatedError, ForbiddenError, createAuthError } from './errors.js';
|
|
36
37
|
import { isPolicyExpression, createRuleFromExpression, evaluateExpression } from './expressions.js';
|
|
37
38
|
import {
|
|
@@ -169,33 +170,14 @@ const executeRule = async (rule, parent, args, ctx, info) => {
|
|
|
169
170
|
};
|
|
170
171
|
|
|
171
172
|
/**
|
|
172
|
-
* Creates a graphql-middleware compatible authorization middleware
|
|
173
|
+
* Creates a graphql-middleware compatible authorization middleware.
|
|
174
|
+
*
|
|
175
|
+
* @deprecated Use {@link createAuthPlugin} instead. `applyMiddleware` from graphql-middleware
|
|
176
|
+
* can cause duplicate-type errors when the schema contains custom introspection extensions.
|
|
173
177
|
*
|
|
174
178
|
* @param {PermissionSchema} permissions - The permission schema object
|
|
175
179
|
* @param {AuthMiddlewareOptions} [options={}] - Middleware options
|
|
176
180
|
* @returns {Function} A graphql-middleware compatible middleware function
|
|
177
|
-
*
|
|
178
|
-
* @example
|
|
179
|
-
* const permissions = {
|
|
180
|
-
* Query: {
|
|
181
|
-
* users: requireAuth(),
|
|
182
|
-
* adminDashboard: requireRole('ADMIN')
|
|
183
|
-
* },
|
|
184
|
-
* User: {
|
|
185
|
-
* '*': requireAuth(),
|
|
186
|
-
* email: requireRole('ADMIN')
|
|
187
|
-
* },
|
|
188
|
-
* Post: {
|
|
189
|
-
* content: {
|
|
190
|
-
* anyOf: [
|
|
191
|
-
* { eq: [{ ref: 'parent.published' }, true] },
|
|
192
|
-
* { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] }
|
|
193
|
-
* ]
|
|
194
|
-
* }
|
|
195
|
-
* }
|
|
196
|
-
* };
|
|
197
|
-
*
|
|
198
|
-
* const middleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
|
|
199
181
|
*/
|
|
200
182
|
export const createAuthMiddleware = (permissions, options = {}) => {
|
|
201
183
|
const {
|
|
@@ -250,8 +232,11 @@ export const createAuthMiddleware = (permissions, options = {}) => {
|
|
|
250
232
|
};
|
|
251
233
|
|
|
252
234
|
/**
|
|
253
|
-
* Creates a field-level middleware object from a permission schema
|
|
254
|
-
* This can be used with graphql-middleware's applyMiddleware
|
|
235
|
+
* Creates a field-level middleware object from a permission schema.
|
|
236
|
+
* This can be used with graphql-middleware's applyMiddleware.
|
|
237
|
+
*
|
|
238
|
+
* @deprecated Use {@link createAuthPlugin} instead. `applyMiddleware` from graphql-middleware
|
|
239
|
+
* can cause duplicate-type errors when the schema contains custom introspection extensions.
|
|
255
240
|
*
|
|
256
241
|
* @param {PermissionSchema} permissions - The permission schema
|
|
257
242
|
* @param {AuthMiddlewareOptions} [options={}] - Middleware options
|
|
@@ -277,9 +262,103 @@ export const createFieldMiddleware = (permissions, options = {}) => {
|
|
|
277
262
|
return fieldMiddleware;
|
|
278
263
|
};
|
|
279
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Creates an Envelop-compatible authorization plugin that wraps schema resolvers in-place.
|
|
267
|
+
*
|
|
268
|
+
* Unlike {@link createAuthMiddleware} (which requires graphql-middleware's `applyMiddleware`
|
|
269
|
+
* and rebuilds the schema), this plugin mutates resolvers directly on the existing schema,
|
|
270
|
+
* avoiding schema reconstruction and the duplicate-type errors it can cause.
|
|
271
|
+
*
|
|
272
|
+
* @param {PermissionSchema} permissions - The permission schema object
|
|
273
|
+
* @param {AuthMiddlewareOptions} [options={}] - Plugin options
|
|
274
|
+
* @returns {Object} An Envelop plugin with an `onSchemaChange` hook
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* import { auth } from '@simtlix/simfinity-js';
|
|
278
|
+
* import { createYoga } from 'graphql-yoga';
|
|
279
|
+
*
|
|
280
|
+
* const permissions = {
|
|
281
|
+
* Query: {
|
|
282
|
+
* users: requireAuth(),
|
|
283
|
+
* adminDashboard: requireRole('ADMIN')
|
|
284
|
+
* },
|
|
285
|
+
* User: {
|
|
286
|
+
* '*': requireAuth(),
|
|
287
|
+
* email: requireRole('ADMIN')
|
|
288
|
+
* }
|
|
289
|
+
* };
|
|
290
|
+
*
|
|
291
|
+
* const authPlugin = auth.createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
|
|
292
|
+
* const yoga = createYoga({ schema, plugins: [authPlugin] });
|
|
293
|
+
*/
|
|
294
|
+
export const createAuthPlugin = (permissions, options = {}) => {
|
|
295
|
+
const {
|
|
296
|
+
defaultPolicy = 'DENY',
|
|
297
|
+
debug = false,
|
|
298
|
+
} = options;
|
|
299
|
+
|
|
300
|
+
const log = debug ? console.log.bind(console, '[auth]') : () => {};
|
|
301
|
+
const processedSchemas = new WeakSet();
|
|
302
|
+
|
|
303
|
+
const wrapSchemaResolvers = (schema) => {
|
|
304
|
+
if (processedSchemas.has(schema)) return;
|
|
305
|
+
|
|
306
|
+
const typeMap = schema.getTypeMap();
|
|
307
|
+
|
|
308
|
+
for (const [typeName, type] of Object.entries(typeMap)) {
|
|
309
|
+
if (!(type instanceof GraphQLObjectType) || typeName.startsWith('__')) continue;
|
|
310
|
+
|
|
311
|
+
const fields = type.getFields();
|
|
312
|
+
|
|
313
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
314
|
+
const rules = getFieldRules(permissions, typeName, fieldName);
|
|
315
|
+
const originalResolve = field.resolve || defaultFieldResolver;
|
|
316
|
+
|
|
317
|
+
field.resolve = async (parent, args, ctx, info) => {
|
|
318
|
+
log(`Checking ${typeName}.${fieldName}`);
|
|
319
|
+
|
|
320
|
+
if (rules === null || rules.length === 0) {
|
|
321
|
+
log(`No rules for ${typeName}.${fieldName}, applying default policy: ${defaultPolicy}`);
|
|
322
|
+
|
|
323
|
+
if (defaultPolicy === 'DENY') {
|
|
324
|
+
throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return originalResolve(parent, args, ctx, info);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const rule of rules) {
|
|
331
|
+
log(`Executing rule for ${typeName}.${fieldName}`);
|
|
332
|
+
|
|
333
|
+
const allowed = await executeRule(rule, parent, args, ctx, info);
|
|
334
|
+
|
|
335
|
+
if (!allowed) {
|
|
336
|
+
log(`Rule denied access to ${typeName}.${fieldName}`);
|
|
337
|
+
throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
log(`Access granted to ${typeName}.${fieldName}`);
|
|
342
|
+
|
|
343
|
+
return originalResolve(parent, args, ctx, info);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
processedSchemas.add(schema);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
onSchemaChange({ schema }) {
|
|
353
|
+
wrapSchemaResolvers(schema);
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
|
|
280
358
|
// Default export with all auth utilities
|
|
281
359
|
const auth = {
|
|
282
|
-
// Main
|
|
360
|
+
// Main factories
|
|
361
|
+
createAuthPlugin,
|
|
283
362
|
createAuthMiddleware,
|
|
284
363
|
createFieldMiddleware,
|
|
285
364
|
|
package/src/plugins.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createAuthPlugin } from './auth/index.js';
|
|
2
|
+
|
|
3
|
+
export { createAuthPlugin } from './auth/index.js';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Apollo Server plugin to add count to GraphQL response extensions
|
|
3
7
|
* @returns {Object} Apollo Server plugin
|
|
@@ -40,11 +44,10 @@ export const envelopCountPlugin = () => {
|
|
|
40
44
|
};
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
// Export all plugins as an object for convenience
|
|
44
47
|
const plugins = {
|
|
48
|
+
createAuthPlugin,
|
|
45
49
|
apolloCountPlugin,
|
|
46
50
|
envelopCountPlugin,
|
|
47
51
|
};
|
|
48
52
|
|
|
49
53
|
export default plugins;
|
|
50
|
-
|