@stonecrop/casl-middleware 0.7.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.
Files changed (66) hide show
  1. package/README.md +149 -0
  2. package/dist/casl-middleware.d.ts +158 -0
  3. package/dist/casl-middleware.js +40571 -0
  4. package/dist/casl-middleware.js.map +1 -0
  5. package/dist/casl_middleware.tsbuildinfo +1 -0
  6. package/dist/src/index.d.ts +6 -0
  7. package/dist/src/index.d.ts.map +1 -0
  8. package/dist/src/index.js +3 -0
  9. package/dist/src/middleware/ability.d.ts +55 -0
  10. package/dist/src/middleware/ability.d.ts.map +1 -0
  11. package/dist/src/middleware/ability.js +139 -0
  12. package/dist/src/middleware/graphql.d.ts +11 -0
  13. package/dist/src/middleware/graphql.d.ts.map +1 -0
  14. package/dist/src/middleware/graphql.js +120 -0
  15. package/dist/src/middleware/introspection.d.ts +71 -0
  16. package/dist/src/middleware/introspection.d.ts.map +1 -0
  17. package/dist/src/middleware/introspection.js +169 -0
  18. package/dist/src/middleware/jwt.d.ts +114 -0
  19. package/dist/src/middleware/jwt.d.ts.map +1 -0
  20. package/dist/src/middleware/jwt.js +291 -0
  21. package/dist/src/middleware/postgraphile.d.ts +7 -0
  22. package/dist/src/middleware/postgraphile.d.ts.map +1 -0
  23. package/dist/src/middleware/postgraphile.js +80 -0
  24. package/dist/src/middleware/yoga.d.ts +15 -0
  25. package/dist/src/middleware/yoga.d.ts.map +1 -0
  26. package/dist/src/middleware/yoga.js +32 -0
  27. package/dist/src/tsdoc-metadata.json +11 -0
  28. package/dist/src/types/index.d.ts +114 -0
  29. package/dist/src/types/index.d.ts.map +1 -0
  30. package/dist/src/types/index.js +0 -0
  31. package/dist/tests/ability.test.d.ts +2 -0
  32. package/dist/tests/ability.test.d.ts.map +1 -0
  33. package/dist/tests/ability.test.js +125 -0
  34. package/dist/tests/helpers/test-utils.d.ts +46 -0
  35. package/dist/tests/helpers/test-utils.d.ts.map +1 -0
  36. package/dist/tests/helpers/test-utils.js +92 -0
  37. package/dist/tests/introspection.test.d.ts +2 -0
  38. package/dist/tests/introspection.test.d.ts.map +1 -0
  39. package/dist/tests/introspection.test.js +368 -0
  40. package/dist/tests/jwt.test.d.ts +2 -0
  41. package/dist/tests/jwt.test.d.ts.map +1 -0
  42. package/dist/tests/jwt.test.js +371 -0
  43. package/dist/tests/middleware.test.d.ts +2 -0
  44. package/dist/tests/middleware.test.d.ts.map +1 -0
  45. package/dist/tests/middleware.test.js +184 -0
  46. package/dist/tests/postgraphile-plugin.test.d.ts +2 -0
  47. package/dist/tests/postgraphile-plugin.test.d.ts.map +1 -0
  48. package/dist/tests/postgraphile-plugin.test.js +56 -0
  49. package/dist/tests/setup.d.ts +2 -0
  50. package/dist/tests/setup.d.ts.map +1 -0
  51. package/dist/tests/setup.js +11 -0
  52. package/dist/tests/user-roles.test.d.ts +2 -0
  53. package/dist/tests/user-roles.test.d.ts.map +1 -0
  54. package/dist/tests/user-roles.test.js +157 -0
  55. package/dist/tests/yoga-plugin.test.d.ts +2 -0
  56. package/dist/tests/yoga-plugin.test.d.ts.map +1 -0
  57. package/dist/tests/yoga-plugin.test.js +47 -0
  58. package/package.json +91 -0
  59. package/src/index.ts +15 -0
  60. package/src/middleware/ability.ts +191 -0
  61. package/src/middleware/graphql.ts +157 -0
  62. package/src/middleware/introspection.ts +258 -0
  63. package/src/middleware/jwt.ts +394 -0
  64. package/src/middleware/postgraphile.ts +93 -0
  65. package/src/middleware/yoga.ts +39 -0
  66. package/src/types/index.ts +133 -0
@@ -0,0 +1 @@
1
+ {"version":"5.9.3"}
@@ -0,0 +1,6 @@
1
+ export { createAbility, detectSubjectType } from './middleware/ability';
2
+ export type { AppAbility } from './middleware/ability';
3
+ export { createCaslMiddleware } from './middleware/graphql';
4
+ export { pglCaslPlugin } from './middleware/postgraphile';
5
+ export type { User, Context, MiddlewareOptions, FieldPermission, ResolverFn, MiddlewareFn, PluginOptions, AbilityResponse, CreateAbilityInput, } from './types';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACvE,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,YAAY,EACX,IAAI,EACJ,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,YAAY,EACZ,aAAa,EACb,eAAe,EACf,kBAAkB,GAClB,MAAM,SAAS,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { createAbility, detectSubjectType } from './middleware/ability';
2
+ export { createCaslMiddleware } from './middleware/graphql';
3
+ export { pglCaslPlugin } from './middleware/postgraphile';
@@ -0,0 +1,55 @@
1
+ import { PureAbility } from '@casl/ability';
2
+ import type { Context } from '../types';
3
+ /**
4
+ * Detects the subject type from an object for CASL authorization
5
+ * @param object - The object to detect the subject type from
6
+ * @returns The subject type string
7
+ * @public
8
+ */
9
+ export declare const detectSubjectType: (object: any) => string;
10
+ /**
11
+ * CASL ability type for authorization with flexible subject types
12
+ * @public
13
+ */
14
+ export type AppAbility = PureAbility<[string, any], any>;
15
+ /**
16
+ * Function type for building user abilities
17
+ * @public
18
+ */
19
+ export type AbilityBuilderFunction = (user?: Context['user']) => Promise<AppAbility> | AppAbility;
20
+ /**
21
+ * Default ability builder - only provides minimal public access
22
+ */
23
+ export declare const defaultAbilityBuilder: (user?: Context["user"]) => AppAbility;
24
+ /**
25
+ * Create ability using a provided builder function
26
+ * @param user - User information for building the ability
27
+ * @param builderFn - Function to build the ability
28
+ * @returns Promise resolving to the created ability
29
+ * @public
30
+ */
31
+ export declare const createAbility: (user?: Context["user"], builderFn?: AbilityBuilderFunction) => Promise<AppAbility>;
32
+ /**
33
+ * Factory function for creating custom ability builders
34
+ */
35
+ export declare const createDatabaseAbilityBuilder: (fetchRules: (userId?: string) => Promise<any[]>) => AbilityBuilderFunction;
36
+ /**
37
+ * Create an ability builder from static configuration
38
+ */
39
+ export declare const createConfigBasedAbilityBuilder: (config: {
40
+ roles: Record<string, Array<{
41
+ action: string | string[];
42
+ subject: string;
43
+ fields?: string | string[];
44
+ conditions?: any;
45
+ inverted?: boolean;
46
+ }>>;
47
+ defaultRules?: Array<{
48
+ action: string | string[];
49
+ subject: string;
50
+ fields?: string | string[];
51
+ conditions?: any;
52
+ }>;
53
+ }) => AbilityBuilderFunction;
54
+ export declare const createAbilityFactory: <T extends PureAbility>(builderFn: (user?: Context["user"]) => T) => ((user?: Context["user"]) => T);
55
+ //# sourceMappingURL=ability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.d.ts","sourceRoot":"","sources":["../../../src/middleware/ability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,WAAW,EAAgE,MAAM,eAAe,CAAA;AAEzH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,GAAG,KAAG,MAc/C,CAAA;AAwBD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAA;AAExD;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAAA;AAIjG;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,OAAO,CAAC,MAAM,CAAC,KAAG,UAU9D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GACzB,OAAO,OAAO,CAAC,MAAM,CAAC,EACtB,YAAW,sBAA8C,KACvD,OAAO,CAAC,UAAU,CAEpB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,4BAA4B,GACxC,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,KAC7C,sBAwBF,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,+BAA+B,GAAI,QAAQ;IACvD,KAAK,EAAE,MAAM,CACZ,MAAM,EACN,KAAK,CAAC;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;QACzB,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;QAC1B,UAAU,CAAC,EAAE,GAAG,CAAA;QAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;KAClB,CAAC,CACF,CAAA;IACD,YAAY,CAAC,EAAE,KAAK,CAAC;QACpB,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;QACzB,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;QAC1B,UAAU,CAAC,EAAE,GAAG,CAAA;KAChB,CAAC,CAAA;CACF,KAAG,sBA0CH,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,WAAW,EACzD,WAAW,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KACtC,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAEhC,CAAA"}
@@ -0,0 +1,139 @@
1
+ import { AbilityBuilder, PureAbility } from '@casl/ability';
2
+ /**
3
+ * Detects the subject type from an object for CASL authorization
4
+ * @param object - The object to detect the subject type from
5
+ * @returns The subject type string
6
+ * @public
7
+ */
8
+ export const detectSubjectType = (object) => {
9
+ // Handle CASL's subject() helper objects
10
+ if (object && typeof object === 'object') {
11
+ // CASL uses __caslSubjectType__ property
12
+ if ('__caslSubjectType__' in object) {
13
+ return object.__caslSubjectType__;
14
+ }
15
+ // Fallback to other type indicators
16
+ if (object.type)
17
+ return object.type;
18
+ if (object.constructor?.name && object.constructor.name !== 'Object') {
19
+ return object.constructor.name;
20
+ }
21
+ }
22
+ return 'Unknown';
23
+ };
24
+ // Simple field matcher - checks if a field is in the restricted list
25
+ const fieldMatcher = fields => field => {
26
+ if (!fields || !field)
27
+ return false;
28
+ if (Array.isArray(fields)) {
29
+ return fields.includes(field);
30
+ }
31
+ return fields === field;
32
+ };
33
+ // Simple conditions matcher - basic equality check
34
+ const conditionsMatcher = conditions => object => {
35
+ if (!conditions || !object)
36
+ return true;
37
+ return Object.keys(conditions).every(key => {
38
+ const conditionValue = conditions[key];
39
+ const objectValue = object[key];
40
+ // Simple equality check
41
+ return objectValue === conditionValue;
42
+ });
43
+ };
44
+ const Ability = PureAbility;
45
+ /**
46
+ * Default ability builder - only provides minimal public access
47
+ */
48
+ export const defaultAbilityBuilder = (user) => {
49
+ const { can, build } = new AbilityBuilder(Ability);
50
+ can('read', 'Query');
51
+ return build({
52
+ detectSubjectType,
53
+ fieldMatcher,
54
+ conditionsMatcher,
55
+ });
56
+ };
57
+ /**
58
+ * Create ability using a provided builder function
59
+ * @param user - User information for building the ability
60
+ * @param builderFn - Function to build the ability
61
+ * @returns Promise resolving to the created ability
62
+ * @public
63
+ */
64
+ export const createAbility = async (user, builderFn = defaultAbilityBuilder) => {
65
+ return await builderFn(user);
66
+ };
67
+ /**
68
+ * Factory function for creating custom ability builders
69
+ */
70
+ export const createDatabaseAbilityBuilder = (fetchRules) => {
71
+ return async (user) => {
72
+ const { can, cannot, build } = new AbilityBuilder(Ability);
73
+ if (user?.id) {
74
+ const rules = await fetchRules(user.id);
75
+ rules.forEach(rule => {
76
+ if (rule.inverted) {
77
+ cannot(rule.action, rule.subject, rule.fields, rule.conditions);
78
+ }
79
+ else {
80
+ can(rule.action, rule.subject, rule.fields, rule.conditions);
81
+ }
82
+ });
83
+ }
84
+ else {
85
+ can('read', 'Query');
86
+ }
87
+ return build({
88
+ detectSubjectType,
89
+ fieldMatcher,
90
+ conditionsMatcher,
91
+ });
92
+ };
93
+ };
94
+ /**
95
+ * Create an ability builder from static configuration
96
+ */
97
+ export const createConfigBasedAbilityBuilder = (config) => {
98
+ return (user) => {
99
+ const { can, cannot, build } = new AbilityBuilder(Ability);
100
+ // Apply default rules
101
+ if (config.defaultRules) {
102
+ config.defaultRules.forEach(rule => {
103
+ can(rule.action, rule.subject, rule.fields, rule.conditions);
104
+ });
105
+ }
106
+ // Apply role-based rules
107
+ if (user?.roles) {
108
+ user.roles.forEach(role => {
109
+ const rules = config.roles[role];
110
+ if (rules) {
111
+ rules.forEach(rule => {
112
+ // Process template variables in conditions
113
+ let processedConditions = rule.conditions;
114
+ if (processedConditions && user.id) {
115
+ const condStr = JSON.stringify(processedConditions);
116
+ if (condStr.includes('{{userId}}')) {
117
+ processedConditions = JSON.parse(condStr.replace(/{{userId}}/g, user.id));
118
+ }
119
+ }
120
+ if (rule.inverted) {
121
+ cannot(rule.action, rule.subject, rule.fields, processedConditions);
122
+ }
123
+ else {
124
+ can(rule.action, rule.subject, rule.fields, processedConditions);
125
+ }
126
+ });
127
+ }
128
+ });
129
+ }
130
+ return build({
131
+ detectSubjectType,
132
+ fieldMatcher,
133
+ conditionsMatcher,
134
+ });
135
+ };
136
+ };
137
+ export const createAbilityFactory = (builderFn) => {
138
+ return (user) => builderFn(user);
139
+ };
@@ -0,0 +1,11 @@
1
+ import { GraphQLResolveInfo } from 'graphql';
2
+ import { Context, MiddlewareOptions, ResolverFn } from '../types';
3
+ /**
4
+ * Creates CASL authorization middleware for GraphQL resolvers
5
+ * @param options - Configuration options for the middleware
6
+ * @returns Middleware function that wraps resolvers with authorization checks
7
+ * @public
8
+ */
9
+ export declare const createCaslMiddleware: (options?: MiddlewareOptions) => (resolve: ResolverFn, root: any, args: any, context: Context, info: GraphQLResolveInfo) => Promise<any>;
10
+ export declare const combineMiddlewares: (...middlewares: any[]) => (resolve: ResolverFn, root: any, args: any, context: any, info: any) => any;
11
+ //# sourceMappingURL=graphql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../../src/middleware/graphql.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAyDjE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,UAAS,iBAAsB,MAclE,SAAS,UAAU,EACnB,MAAM,GAAG,EACT,MAAM,GAAG,EACT,SAAS,OAAO,EAChB,MAAM,kBAAkB,KACtB,OAAO,CAAC,GAAG,CA4Dd,CAAA;AAGD,eAAO,MAAM,kBAAkB,GAAI,GAAG,aAAa,GAAG,EAAE,MAC/C,SAAS,UAAU,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE,SAAS,GAAG,EAAE,MAAM,GAAG,QAO1E,CAAA"}
@@ -0,0 +1,120 @@
1
+ import { subject } from '@casl/ability';
2
+ import { GraphQLError } from 'graphql';
3
+ import { parse, visit } from 'graphql/language';
4
+ import { createAbility, defaultAbilityBuilder } from './ability';
5
+ // Helper to extract operation name from GraphQL info
6
+ const getOperationName = (info) => {
7
+ return info.operation.operation.toLowerCase();
8
+ };
9
+ // Helper to get full field path
10
+ const getFieldPath = (info) => {
11
+ return info.path.typename ? `${info.path.typename}.${info.path.key}` : String(info.path.key);
12
+ };
13
+ // Parse GraphQL query to extract accessed fields
14
+ const extractAccessedFields = (query) => {
15
+ const fields = [];
16
+ try {
17
+ const ast = parse(query);
18
+ visit(ast, {
19
+ Field: {
20
+ enter(node) {
21
+ if (node.name.value !== '__typename') {
22
+ fields.push(node.name.value);
23
+ }
24
+ },
25
+ },
26
+ });
27
+ }
28
+ catch (error) {
29
+ console.error('Failed to parse GraphQL query:', error);
30
+ }
31
+ return fields;
32
+ };
33
+ // Check if ability allows the operation
34
+ const checkPermission = (context, action, subjectName, conditions) => {
35
+ if (!context.ability) {
36
+ return false;
37
+ }
38
+ try {
39
+ // Only use subject helper if we have specific conditions to check
40
+ // (not just resolver args, but actual CASL conditions)
41
+ if (conditions && typeof conditions === 'object') {
42
+ return context.ability.can(action, subject(subjectName, conditions));
43
+ }
44
+ else {
45
+ // Simple check without conditions - just action and subject type
46
+ return context.ability.can(action, subjectName);
47
+ }
48
+ }
49
+ catch (error) {
50
+ console.error('Permission check failed:', error);
51
+ return false;
52
+ }
53
+ };
54
+ /**
55
+ * Creates CASL authorization middleware for GraphQL resolvers
56
+ * @param options - Configuration options for the middleware
57
+ * @returns Middleware function that wraps resolvers with authorization checks
58
+ * @public
59
+ */
60
+ export const createCaslMiddleware = (options = {}) => {
61
+ const { subjectMap = {}, actionMap = {
62
+ query: 'read',
63
+ mutation: 'update',
64
+ subscription: 'read',
65
+ }, fieldPermissions = {}, abilityBuilder = defaultAbilityBuilder, debug = false, } = options;
66
+ return async (resolve, root, args, context, info) => {
67
+ // Create ability if not already in context
68
+ if (!context.ability) {
69
+ context.ability = await createAbility(context.user, abilityBuilder);
70
+ }
71
+ const operation = getOperationName(info);
72
+ const fieldPath = getFieldPath(info);
73
+ const action = actionMap[operation] || operation;
74
+ // Get subject from map or default to type name
75
+ const subjectType = info.parentType.name;
76
+ const subjectName = subjectMap[subjectType] || subjectType;
77
+ if (debug) {
78
+ console.log(`Checking permission: ${action} on ${subjectName} at ${fieldPath}`);
79
+ }
80
+ // Check field-level permissions if defined
81
+ if (fieldPermissions[fieldPath]) {
82
+ const permissions = fieldPermissions[fieldPath];
83
+ const allowed = permissions.some(permission =>
84
+ // Don't pass resolver args here - only use them if the permission defines conditions
85
+ checkPermission(context, permission.action, permission.subject, permission.conditions));
86
+ if (!allowed) {
87
+ throw new GraphQLError(`Access denied for field ${fieldPath}`);
88
+ }
89
+ }
90
+ // Check operation-level permission
91
+ // Don't pass resolver args as conditions - they're not CASL conditions
92
+ const allowed = checkPermission(context, action, subjectName);
93
+ if (!allowed) {
94
+ throw new GraphQLError(`Access denied for ${action} on ${subjectName}`);
95
+ }
96
+ // Additional checks for mutations with input fields
97
+ if (operation === 'mutation' && args.input) {
98
+ const querySource = info.operation.loc?.source.body;
99
+ if (querySource) {
100
+ const accessedFields = extractAccessedFields(querySource);
101
+ for (const field of accessedFields) {
102
+ // Check field-level update permissions if needed
103
+ const fieldAllowed = checkPermission(context, 'update', subjectName, { field });
104
+ if (!fieldAllowed && debug) {
105
+ console.warn(`Warning: Field ${field} update not explicitly allowed`);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ // Call the resolver
111
+ return resolve(root, args, context, info);
112
+ };
113
+ };
114
+ // Utility function to combine multiple middlewares
115
+ export const combineMiddlewares = (...middlewares) => {
116
+ return (resolve, root, args, context, info) => {
117
+ const chain = middlewares.reduceRight((next, middleware) => () => middleware(next, root, args, context, info), () => resolve(root, args, context, info));
118
+ return chain();
119
+ };
120
+ };
@@ -0,0 +1,71 @@
1
+ import type { Context, MiddlewareOptions } from '../types';
2
+ export interface IntrospectionConfig {
3
+ /**
4
+ * Whether to allow introspection queries at all
5
+ * @default true in development, false in production
6
+ */
7
+ enabled?: boolean;
8
+ /**
9
+ * Roles that are allowed to introspect the schema
10
+ * If undefined, all authenticated users can introspect
11
+ */
12
+ allowedRoles?: string[];
13
+ /**
14
+ * Allow unauthenticated introspection
15
+ * @default false
16
+ */
17
+ allowAnonymous?: boolean;
18
+ /**
19
+ * Custom function to determine if introspection is allowed
20
+ */
21
+ customCheck?: (context: Context) => boolean | Promise<boolean>;
22
+ /**
23
+ * Types to hide from introspection based on user permissions
24
+ * Maps type names to required permissions
25
+ */
26
+ typePermissions?: Record<string, {
27
+ action: string;
28
+ subject: string;
29
+ }>;
30
+ /**
31
+ * Fields to hide from introspection based on user permissions
32
+ * Maps "Type.field" to required permissions
33
+ */
34
+ fieldPermissions?: Record<string, {
35
+ action: string;
36
+ subject: string;
37
+ }>;
38
+ }
39
+ /**
40
+ * Middleware to restrict GraphQL introspection based on user permissions
41
+ */
42
+ export declare const createIntrospectionMiddleware: (config?: IntrospectionConfig) => (resolve: any, root: any, args: any, context: Context, info: any) => Promise<any>;
43
+ /**
44
+ * Postgraphile plugin for introspection control
45
+ */
46
+ export declare const createPostgraphileIntrospectionPlugin: (config: IntrospectionConfig) => {
47
+ name: string;
48
+ version: string;
49
+ grafast: {
50
+ hooks: {
51
+ GraphQLSchema(schema: any): any;
52
+ };
53
+ };
54
+ };
55
+ /**
56
+ * Utility to create ability rules for introspection
57
+ */
58
+ export declare const createIntrospectionAbilityRules: (user?: {
59
+ roles?: string[];
60
+ }) => Array<{
61
+ action: string;
62
+ subject: string;
63
+ }>;
64
+ /**
65
+ * Example: Combine introspection with CASL middleware
66
+ */
67
+ export declare const createSecureGraphQLMiddleware: (options: {
68
+ casl?: MiddlewareOptions;
69
+ introspection?: IntrospectionConfig;
70
+ }) => (resolve: any, root: any, args: any, context: any, info: any) => any;
71
+ //# sourceMappingURL=introspection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../../../src/middleware/introspection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE1D,MAAM,WAAW,mBAAmB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IAEvB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAE9D;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAErE;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACtE;AAED;;GAEG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAQ,mBAAwB,MAU/D,SAAS,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE,SAAS,OAAO,EAAE,MAAM,GAAG,iBA8D7E,CAAA;AAyDD;;GAEG;AACH,eAAO,MAAM,qCAAqC,GAAI,QAAQ,mBAAmB;;;;;kCAQvD,GAAG;;;CAW5B,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,+BAA+B,GAAI,OAAO;IACtD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB,KAAG,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAgC5C,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAS;IACtD,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,aAAa,CAAC,EAAE,mBAAmB,CAAA;CACnC,MASQ,SAAS,GAAG,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE,SAAS,GAAG,EAAE,MAAM,GAAG,QAOnE,CAAA"}
@@ -0,0 +1,169 @@
1
+ import { GraphQLError } from 'graphql';
2
+ /**
3
+ * Middleware to restrict GraphQL introspection based on user permissions
4
+ */
5
+ export const createIntrospectionMiddleware = (config = {}) => {
6
+ const { enabled = process.env.NODE_ENV !== 'production', allowedRoles, allowAnonymous = false, customCheck, typePermissions = {}, fieldPermissions = {}, } = config;
7
+ return async (resolve, root, args, context, info) => {
8
+ // Check if this is an introspection query
9
+ const isIntrospection = info.fieldName === '__schema' ||
10
+ info.fieldName === '__type' ||
11
+ info.parentType?.name === '__Schema' ||
12
+ info.parentType?.name === '__Type';
13
+ if (!isIntrospection) {
14
+ // Not an introspection query, continue normally
15
+ return resolve(root, args, context, info);
16
+ }
17
+ // Check if introspection is enabled
18
+ if (!enabled) {
19
+ throw new GraphQLError('Introspection is disabled');
20
+ }
21
+ // Custom check function
22
+ if (customCheck) {
23
+ const allowed = await customCheck(context);
24
+ if (!allowed) {
25
+ throw new GraphQLError('Introspection not allowed');
26
+ }
27
+ }
28
+ // Check authentication
29
+ if (!context.user && !allowAnonymous) {
30
+ throw new GraphQLError('Authentication required for introspection');
31
+ }
32
+ // Check role-based access
33
+ if (allowedRoles && allowedRoles.length > 0) {
34
+ const userRoles = context.user?.roles || [];
35
+ const hasAllowedRole = allowedRoles.some(role => userRoles.includes(role));
36
+ if (!hasAllowedRole) {
37
+ throw new GraphQLError('Insufficient permissions for introspection');
38
+ }
39
+ }
40
+ // Check CASL-based permissions
41
+ if (context.ability) {
42
+ // Check if user can read schema
43
+ if (!context.ability.can('read', '__Schema')) {
44
+ throw new GraphQLError('Permission denied for schema introspection');
45
+ }
46
+ }
47
+ // Get the result
48
+ let result = await resolve(root, args, context, info);
49
+ // Filter the result based on permissions
50
+ if (result && (typePermissions || fieldPermissions)) {
51
+ result = filterIntrospectionResult(result, context, {
52
+ typePermissions,
53
+ fieldPermissions,
54
+ });
55
+ }
56
+ return result;
57
+ };
58
+ };
59
+ /**
60
+ * Filter introspection results based on user permissions
61
+ */
62
+ function filterIntrospectionResult(result, context, config) {
63
+ if (!context.ability)
64
+ return result;
65
+ // Filter __schema result
66
+ if (result && result.types) {
67
+ result.types = result.types.filter((type) => {
68
+ // Check if user has permission to see this type
69
+ const permission = config.typePermissions?.[type.name];
70
+ if (permission) {
71
+ return context.ability.can(permission.action, permission.subject);
72
+ }
73
+ return true; // Show types without specific permissions
74
+ });
75
+ // Filter fields within types
76
+ result.types.forEach((type) => {
77
+ if (type.fields) {
78
+ type.fields = type.fields.filter((field) => {
79
+ const fieldKey = `${type.name}.${field.name}`;
80
+ const permission = config.fieldPermissions?.[fieldKey];
81
+ if (permission) {
82
+ return context.ability.can(permission.action, permission.subject);
83
+ }
84
+ return true;
85
+ });
86
+ }
87
+ });
88
+ }
89
+ // Filter __type result
90
+ if (result && result.fields) {
91
+ const typeName = result.name;
92
+ result.fields = result.fields.filter((field) => {
93
+ const fieldKey = `${typeName}.${field.name}`;
94
+ const permission = config.fieldPermissions?.[fieldKey];
95
+ if (permission) {
96
+ return context.ability.can(permission.action, permission.subject);
97
+ }
98
+ return true;
99
+ });
100
+ }
101
+ return result;
102
+ }
103
+ /**
104
+ * Postgraphile plugin for introspection control
105
+ */
106
+ export const createPostgraphileIntrospectionPlugin = (config) => {
107
+ return {
108
+ name: 'IntrospectionControlPlugin',
109
+ version: '1.0.0',
110
+ // Disable introspection in GraphiQL based on config
111
+ grafast: {
112
+ hooks: {
113
+ GraphQLSchema(schema) {
114
+ if (!config.enabled) {
115
+ // Remove introspection from schema
116
+ // This is a simplified approach - real implementation would be more complex
117
+ console.warn('Introspection control in Postgraphile requires custom implementation');
118
+ }
119
+ return schema;
120
+ },
121
+ },
122
+ },
123
+ };
124
+ };
125
+ /**
126
+ * Utility to create ability rules for introspection
127
+ */
128
+ export const createIntrospectionAbilityRules = (user) => {
129
+ const rules = [];
130
+ if (!user) {
131
+ // Anonymous users cannot introspect
132
+ return rules;
133
+ }
134
+ const roles = user.roles || [];
135
+ // Admins can introspect everything
136
+ if (roles.includes('admin')) {
137
+ rules.push({ action: 'read', subject: '__Schema' });
138
+ rules.push({ action: 'read', subject: '__Type' });
139
+ return rules;
140
+ }
141
+ // Developers can introspect
142
+ if (roles.includes('developer')) {
143
+ rules.push({ action: 'read', subject: '__Schema' });
144
+ rules.push({ action: 'read', subject: '__Type' });
145
+ return rules;
146
+ }
147
+ // Regular users get limited introspection
148
+ if (roles.includes('user')) {
149
+ // They can see the schema but not all types
150
+ rules.push({ action: 'read', subject: '__Schema' });
151
+ // Specific types they can see would be added here
152
+ }
153
+ return rules;
154
+ };
155
+ /**
156
+ * Example: Combine introspection with CASL middleware
157
+ */
158
+ export const createSecureGraphQLMiddleware = (options) => {
159
+ const middlewares = [];
160
+ // Add introspection control
161
+ if (options.introspection) {
162
+ middlewares.push(createIntrospectionMiddleware(options.introspection));
163
+ }
164
+ // Combine all middlewares
165
+ return (resolve, root, args, context, info) => {
166
+ const chain = middlewares.reduceRight((next, middleware) => () => middleware(next, root, args, context, info), () => resolve(root, args, context, info));
167
+ return chain();
168
+ };
169
+ };