@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.
- package/README.md +149 -0
- package/dist/casl-middleware.d.ts +158 -0
- package/dist/casl-middleware.js +40571 -0
- package/dist/casl-middleware.js.map +1 -0
- package/dist/casl_middleware.tsbuildinfo +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/middleware/ability.d.ts +55 -0
- package/dist/src/middleware/ability.d.ts.map +1 -0
- package/dist/src/middleware/ability.js +139 -0
- package/dist/src/middleware/graphql.d.ts +11 -0
- package/dist/src/middleware/graphql.d.ts.map +1 -0
- package/dist/src/middleware/graphql.js +120 -0
- package/dist/src/middleware/introspection.d.ts +71 -0
- package/dist/src/middleware/introspection.d.ts.map +1 -0
- package/dist/src/middleware/introspection.js +169 -0
- package/dist/src/middleware/jwt.d.ts +114 -0
- package/dist/src/middleware/jwt.d.ts.map +1 -0
- package/dist/src/middleware/jwt.js +291 -0
- package/dist/src/middleware/postgraphile.d.ts +7 -0
- package/dist/src/middleware/postgraphile.d.ts.map +1 -0
- package/dist/src/middleware/postgraphile.js +80 -0
- package/dist/src/middleware/yoga.d.ts +15 -0
- package/dist/src/middleware/yoga.d.ts.map +1 -0
- package/dist/src/middleware/yoga.js +32 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/types/index.d.ts +114 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +0 -0
- package/dist/tests/ability.test.d.ts +2 -0
- package/dist/tests/ability.test.d.ts.map +1 -0
- package/dist/tests/ability.test.js +125 -0
- package/dist/tests/helpers/test-utils.d.ts +46 -0
- package/dist/tests/helpers/test-utils.d.ts.map +1 -0
- package/dist/tests/helpers/test-utils.js +92 -0
- package/dist/tests/introspection.test.d.ts +2 -0
- package/dist/tests/introspection.test.d.ts.map +1 -0
- package/dist/tests/introspection.test.js +368 -0
- package/dist/tests/jwt.test.d.ts +2 -0
- package/dist/tests/jwt.test.d.ts.map +1 -0
- package/dist/tests/jwt.test.js +371 -0
- package/dist/tests/middleware.test.d.ts +2 -0
- package/dist/tests/middleware.test.d.ts.map +1 -0
- package/dist/tests/middleware.test.js +184 -0
- package/dist/tests/postgraphile-plugin.test.d.ts +2 -0
- package/dist/tests/postgraphile-plugin.test.d.ts.map +1 -0
- package/dist/tests/postgraphile-plugin.test.js +56 -0
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +11 -0
- package/dist/tests/user-roles.test.d.ts +2 -0
- package/dist/tests/user-roles.test.d.ts.map +1 -0
- package/dist/tests/user-roles.test.js +157 -0
- package/dist/tests/yoga-plugin.test.d.ts +2 -0
- package/dist/tests/yoga-plugin.test.d.ts.map +1 -0
- package/dist/tests/yoga-plugin.test.js +47 -0
- package/package.json +91 -0
- package/src/index.ts +15 -0
- package/src/middleware/ability.ts +191 -0
- package/src/middleware/graphql.ts +157 -0
- package/src/middleware/introspection.ts +258 -0
- package/src/middleware/jwt.ts +394 -0
- package/src/middleware/postgraphile.ts +93 -0
- package/src/middleware/yoga.ts +39 -0
- 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,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
|
+
};
|