@stonecrop/casl-middleware 0.30.0 → 0.32.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/casl-middleware.d.ts +15 -0
- package/dist/casl-middleware.js +3328 -2650
- package/dist/casl-middleware.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/package.json +23 -22
- package/dist/casl-middleware.tsbuildinfo +0 -1
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -3
- package/dist/src/middleware/ability.d.ts +0 -55
- package/dist/src/middleware/ability.d.ts.map +0 -1
- package/dist/src/middleware/ability.js +0 -139
- package/dist/src/middleware/graphql.d.ts +0 -11
- package/dist/src/middleware/graphql.d.ts.map +0 -1
- package/dist/src/middleware/graphql.js +0 -120
- package/dist/src/middleware/introspection.d.ts +0 -71
- package/dist/src/middleware/introspection.d.ts.map +0 -1
- package/dist/src/middleware/introspection.js +0 -169
- package/dist/src/middleware/jwt.d.ts +0 -114
- package/dist/src/middleware/jwt.d.ts.map +0 -1
- package/dist/src/middleware/jwt.js +0 -302
- package/dist/src/middleware/postgraphile.d.ts +0 -7
- package/dist/src/middleware/postgraphile.d.ts.map +0 -1
- package/dist/src/middleware/postgraphile.js +0 -79
- package/dist/src/middleware/yoga.d.ts +0 -15
- package/dist/src/middleware/yoga.d.ts.map +0 -1
- package/dist/src/middleware/yoga.js +0 -30
- package/dist/src/types/index.d.ts +0 -114
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -0
- package/src/index.ts +0 -15
- package/src/middleware/ability.ts +0 -197
- package/src/middleware/graphql.ts +0 -157
- package/src/middleware/introspection.ts +0 -258
- package/src/middleware/jwt.ts +0 -405
- package/src/middleware/postgraphile.ts +0 -89
- package/src/middleware/yoga.ts +0 -36
- package/src/types/index.ts +0 -133
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"yoga.d.ts","sourceRoot":"","sources":["../../../src/middleware/yoga.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAG1C,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAO1D,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,OAAO,CAO1C,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAU,iBAAsB;mBAE1C,GAAG;CAMzB,CAAA"}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { createAbility } from './ability';
|
|
2
|
-
const getLoggedInUser = () => {
|
|
3
|
-
// Mock user for demonstration purposes
|
|
4
|
-
return { id: '1', roles: ['editor'] };
|
|
5
|
-
};
|
|
6
|
-
export const yogaCaslPlugin = {
|
|
7
|
-
onContextBuilding: async ({ extendContext }) => {
|
|
8
|
-
// Make this async
|
|
9
|
-
const user = getLoggedInUser();
|
|
10
|
-
const ability = await createAbility(user); // Await here
|
|
11
|
-
extendContext({ ability, user });
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Create a GraphQL Yoga plugin for CASL authorization
|
|
16
|
-
* Note: This is a placeholder for future implementation
|
|
17
|
-
*
|
|
18
|
-
* @param options - CASL middleware configuration options
|
|
19
|
-
* @returns Yoga plugin
|
|
20
|
-
* @public
|
|
21
|
-
*/
|
|
22
|
-
export const createYogaPlugin = (_options = {}) => {
|
|
23
|
-
return {
|
|
24
|
-
onExecute: async (_) => {
|
|
25
|
-
// TODO: Implement Yoga plugin via createCaslMiddleware(_options).
|
|
26
|
-
// This would integrate with GraphQL Yoga's plugin system.
|
|
27
|
-
console.warn('Yoga plugin not yet implemented');
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
};
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { GraphQLResolveInfo } from 'graphql';
|
|
2
|
-
import type { AppAbility, AbilityBuilderFunction } from '../middleware/ability';
|
|
3
|
-
/**
|
|
4
|
-
* User information for authorization
|
|
5
|
-
* @public
|
|
6
|
-
*/
|
|
7
|
-
export interface User {
|
|
8
|
-
/** Unique identifier for the user */
|
|
9
|
-
id: string;
|
|
10
|
-
/** Array of role names assigned to the user */
|
|
11
|
-
roles?: string[];
|
|
12
|
-
/** Additional user properties */
|
|
13
|
-
[key: string]: any;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* GraphQL context with CASL ability and user information
|
|
17
|
-
* @public
|
|
18
|
-
*/
|
|
19
|
-
export interface Context {
|
|
20
|
-
/** CASL ability instance for authorization checks */
|
|
21
|
-
ability?: AppAbility;
|
|
22
|
-
/** Current authenticated user */
|
|
23
|
-
user?: User;
|
|
24
|
-
/** Additional context properties */
|
|
25
|
-
[key: string]: any;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Configuration options for CASL middleware
|
|
29
|
-
* @public
|
|
30
|
-
*/
|
|
31
|
-
export interface MiddlewareOptions {
|
|
32
|
-
/** Mapping of GraphQL types to authorization subjects */
|
|
33
|
-
subjectMap?: Record<string, string>;
|
|
34
|
-
/** Mapping of GraphQL operations to CASL actions */
|
|
35
|
-
actionMap?: Record<string, string>;
|
|
36
|
-
/** Field-level permission rules */
|
|
37
|
-
fieldPermissions?: Record<string, FieldPermission[]>;
|
|
38
|
-
/** Custom function to build user abilities */
|
|
39
|
-
abilityBuilder?: AbilityBuilderFunction;
|
|
40
|
-
/** Enable debug logging */
|
|
41
|
-
debug?: boolean;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Field-level permission definition for fine-grained access control
|
|
45
|
-
* @public
|
|
46
|
-
*/
|
|
47
|
-
export interface FieldPermission {
|
|
48
|
-
/** CASL action (e.g., 'read', 'write') */
|
|
49
|
-
action: string;
|
|
50
|
-
/** Subject/resource type to apply permission to */
|
|
51
|
-
subject: string;
|
|
52
|
-
/** Specific field name (optional) */
|
|
53
|
-
field?: string;
|
|
54
|
-
/** Conditional rules for permission */
|
|
55
|
-
conditions?: any;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* GraphQL resolver function type
|
|
59
|
-
* @public
|
|
60
|
-
*/
|
|
61
|
-
export type ResolverFn = (root: any, args: any, context: Context, info: GraphQLResolveInfo) => any;
|
|
62
|
-
/**
|
|
63
|
-
* Middleware function that wraps a GraphQL resolver with authorization logic
|
|
64
|
-
* @public
|
|
65
|
-
*/
|
|
66
|
-
export type MiddlewareFn = (resolve: ResolverFn, root: any, args: any, context: Context, info: GraphQLResolveInfo) => any;
|
|
67
|
-
/**
|
|
68
|
-
* Plugin configuration options for framework integrations
|
|
69
|
-
* @public
|
|
70
|
-
*/
|
|
71
|
-
export interface PluginOptions extends MiddlewareOptions {
|
|
72
|
-
/** Custom function to build user abilities */
|
|
73
|
-
abilityBuilder?: AbilityBuilderFunction;
|
|
74
|
-
/** Ability caching configuration */
|
|
75
|
-
cacheOptions?: {
|
|
76
|
-
/** Time-to-live in milliseconds */
|
|
77
|
-
ttl?: number;
|
|
78
|
-
/** Function to generate cache key from user */
|
|
79
|
-
key?: (user?: User) => string;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Response type for ability creation mutations
|
|
84
|
-
* @public
|
|
85
|
-
*/
|
|
86
|
-
export interface AbilityResponse {
|
|
87
|
-
/** Whether the ability was created successfully */
|
|
88
|
-
success: boolean;
|
|
89
|
-
/** The created ability rules */
|
|
90
|
-
ability: any;
|
|
91
|
-
/** Success or error message */
|
|
92
|
-
message: string;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Input type for creating a new ability
|
|
96
|
-
* @public
|
|
97
|
-
*/
|
|
98
|
-
export interface CreateAbilityInput {
|
|
99
|
-
/** User ID to create ability for */
|
|
100
|
-
userId: string;
|
|
101
|
-
/** Array of role names to assign */
|
|
102
|
-
roles: string[];
|
|
103
|
-
}
|
|
104
|
-
export interface AbilityRule {
|
|
105
|
-
id?: string;
|
|
106
|
-
roleId?: string;
|
|
107
|
-
userId?: string;
|
|
108
|
-
action: string | string[];
|
|
109
|
-
subject: string;
|
|
110
|
-
fields?: string[];
|
|
111
|
-
conditions?: any;
|
|
112
|
-
inverted?: boolean;
|
|
113
|
-
}
|
|
114
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/dist/src/types/index.js
DELETED
|
File without changes
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export { createAbility, detectSubjectType } from './middleware/ability'
|
|
2
|
-
export type { AppAbility, AbilityBuilderFunction } from './middleware/ability'
|
|
3
|
-
export { createCaslMiddleware } from './middleware/graphql'
|
|
4
|
-
export { pglCaslPlugin } from './middleware/postgraphile'
|
|
5
|
-
export type {
|
|
6
|
-
User,
|
|
7
|
-
Context,
|
|
8
|
-
MiddlewareOptions,
|
|
9
|
-
FieldPermission,
|
|
10
|
-
ResolverFn,
|
|
11
|
-
MiddlewareFn,
|
|
12
|
-
PluginOptions,
|
|
13
|
-
AbilityResponse,
|
|
14
|
-
CreateAbilityInput,
|
|
15
|
-
} from './types'
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AbilityBuilder,
|
|
3
|
-
PureAbility,
|
|
4
|
-
type AbilityClass,
|
|
5
|
-
type FieldMatcher,
|
|
6
|
-
type ConditionsMatcher,
|
|
7
|
-
} from '@casl/ability'
|
|
8
|
-
|
|
9
|
-
import type { Context } from '../types'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Detects the subject type from an object for CASL authorization
|
|
13
|
-
* @param object - The object to detect the subject type from
|
|
14
|
-
* @returns The subject type string
|
|
15
|
-
* @public
|
|
16
|
-
*/
|
|
17
|
-
export const detectSubjectType = (object: any): string => {
|
|
18
|
-
// Handle CASL's subject() helper objects
|
|
19
|
-
if (object && typeof object === 'object') {
|
|
20
|
-
// CASL uses __caslSubjectType__ property
|
|
21
|
-
if ('__caslSubjectType__' in object) {
|
|
22
|
-
return object.__caslSubjectType__
|
|
23
|
-
}
|
|
24
|
-
// Fallback to other type indicators
|
|
25
|
-
if (object.type) return object.type
|
|
26
|
-
if (object.constructor?.name && object.constructor.name !== 'Object') {
|
|
27
|
-
return object.constructor.name
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return 'Unknown'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Simple field matcher - checks if a field is in the restricted list
|
|
34
|
-
const fieldMatcher: FieldMatcher = fields => field => {
|
|
35
|
-
if (!fields || !field) return false
|
|
36
|
-
if (Array.isArray(fields)) {
|
|
37
|
-
return fields.includes(field)
|
|
38
|
-
}
|
|
39
|
-
return fields === field
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Simple conditions matcher - basic equality check
|
|
43
|
-
const conditionsMatcher: ConditionsMatcher<any> = conditions => object => {
|
|
44
|
-
if (!conditions || !object) return true
|
|
45
|
-
|
|
46
|
-
return Object.keys(conditions).every(key => {
|
|
47
|
-
const conditionValue = conditions[key]
|
|
48
|
-
const objectValue = object[key]
|
|
49
|
-
|
|
50
|
-
// Simple equality check
|
|
51
|
-
return objectValue === conditionValue
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* CASL ability type for authorization with flexible subject types
|
|
57
|
-
* @public
|
|
58
|
-
*/
|
|
59
|
-
export type AppAbility = PureAbility<[string, any], any>
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Function type for building user abilities
|
|
63
|
-
* @public
|
|
64
|
-
*/
|
|
65
|
-
export type AbilityBuilderFunction = (user?: Context['user']) => Promise<AppAbility> | AppAbility
|
|
66
|
-
|
|
67
|
-
const Ability = PureAbility as AbilityClass<AppAbility>
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Default ability builder - only provides minimal public access
|
|
71
|
-
*/
|
|
72
|
-
export const defaultAbilityBuilder = (_user?: Context['user']): AppAbility => {
|
|
73
|
-
const { can, build } = new AbilityBuilder<AppAbility>(Ability)
|
|
74
|
-
|
|
75
|
-
can('read', 'Query')
|
|
76
|
-
|
|
77
|
-
return build({
|
|
78
|
-
detectSubjectType,
|
|
79
|
-
fieldMatcher,
|
|
80
|
-
conditionsMatcher,
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Create ability using a provided builder function
|
|
86
|
-
* @param user - User information for building the ability
|
|
87
|
-
* @param builderFn - Function to build the ability
|
|
88
|
-
* @returns Promise resolving to the created ability
|
|
89
|
-
* @public
|
|
90
|
-
*/
|
|
91
|
-
export const createAbility = async (
|
|
92
|
-
user?: Context['user'],
|
|
93
|
-
builderFn: AbilityBuilderFunction = defaultAbilityBuilder
|
|
94
|
-
): Promise<AppAbility> => {
|
|
95
|
-
return await builderFn(user)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Factory function for creating custom ability builders
|
|
100
|
-
*/
|
|
101
|
-
export const createDatabaseAbilityBuilder = (
|
|
102
|
-
fetchRules: (userId?: string) => Promise<any[]>
|
|
103
|
-
): AbilityBuilderFunction => {
|
|
104
|
-
return async (user?: Context['user']) => {
|
|
105
|
-
const { can, cannot, build } = new AbilityBuilder<AppAbility>(Ability)
|
|
106
|
-
|
|
107
|
-
if (user?.id) {
|
|
108
|
-
const rules = await fetchRules(user.id)
|
|
109
|
-
|
|
110
|
-
rules.forEach(rule => {
|
|
111
|
-
if (rule.inverted) {
|
|
112
|
-
cannot(rule.action, rule.subject, rule.fields, rule.conditions)
|
|
113
|
-
} else {
|
|
114
|
-
can(rule.action, rule.subject, rule.fields, rule.conditions)
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
} else {
|
|
118
|
-
can('read', 'Query')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return build({
|
|
122
|
-
detectSubjectType,
|
|
123
|
-
fieldMatcher,
|
|
124
|
-
conditionsMatcher,
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Create an ability builder from static configuration
|
|
131
|
-
*/
|
|
132
|
-
export const createConfigBasedAbilityBuilder = (config: {
|
|
133
|
-
roles: Record<
|
|
134
|
-
string,
|
|
135
|
-
Array<{
|
|
136
|
-
action: string | string[]
|
|
137
|
-
subject: string
|
|
138
|
-
fields?: string | string[]
|
|
139
|
-
conditions?: any
|
|
140
|
-
inverted?: boolean
|
|
141
|
-
}>
|
|
142
|
-
>
|
|
143
|
-
defaultRules?: Array<{
|
|
144
|
-
action: string | string[]
|
|
145
|
-
subject: string
|
|
146
|
-
fields?: string | string[]
|
|
147
|
-
conditions?: any
|
|
148
|
-
}>
|
|
149
|
-
}): AbilityBuilderFunction => {
|
|
150
|
-
return (user?: Context['user']) => {
|
|
151
|
-
const { can, cannot, build } = new AbilityBuilder<AppAbility>(Ability)
|
|
152
|
-
|
|
153
|
-
// Apply default rules
|
|
154
|
-
if (config.defaultRules) {
|
|
155
|
-
config.defaultRules.forEach(rule => {
|
|
156
|
-
can(rule.action, rule.subject, rule.fields, rule.conditions)
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Apply role-based rules
|
|
161
|
-
if (user?.roles) {
|
|
162
|
-
user.roles.forEach(role => {
|
|
163
|
-
const rules = config.roles[role]
|
|
164
|
-
if (rules) {
|
|
165
|
-
rules.forEach(rule => {
|
|
166
|
-
// Process template variables in conditions
|
|
167
|
-
let processedConditions = rule.conditions
|
|
168
|
-
if (processedConditions && user.id) {
|
|
169
|
-
const condStr = JSON.stringify(processedConditions)
|
|
170
|
-
if (condStr.includes('{{userId}}')) {
|
|
171
|
-
processedConditions = JSON.parse(condStr.replace(/{{userId}}/g, user.id))
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (rule.inverted) {
|
|
176
|
-
cannot(rule.action, rule.subject, rule.fields, processedConditions)
|
|
177
|
-
} else {
|
|
178
|
-
can(rule.action, rule.subject, rule.fields, processedConditions)
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return build({
|
|
186
|
-
detectSubjectType,
|
|
187
|
-
fieldMatcher,
|
|
188
|
-
conditionsMatcher,
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export const createAbilityFactory = <T extends PureAbility>(
|
|
194
|
-
builderFn: (user?: Context['user']) => T
|
|
195
|
-
): ((user?: Context['user']) => T) => {
|
|
196
|
-
return (user?: Context['user']) => builderFn(user)
|
|
197
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { subject } from '@casl/ability'
|
|
2
|
-
import { GraphQLError, GraphQLResolveInfo } from 'graphql'
|
|
3
|
-
import { parse, visit, FieldNode, DocumentNode } from 'graphql/language'
|
|
4
|
-
import { Context, MiddlewareOptions, ResolverFn } from '../types'
|
|
5
|
-
import { createAbility, defaultAbilityBuilder } from './ability'
|
|
6
|
-
|
|
7
|
-
// Helper to extract operation name from GraphQL info
|
|
8
|
-
const getOperationName = (info: GraphQLResolveInfo): string => {
|
|
9
|
-
return info.operation.operation.toLowerCase()
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Helper to get full field path
|
|
13
|
-
const getFieldPath = (info: GraphQLResolveInfo): string => {
|
|
14
|
-
return info.path.typename ? `${info.path.typename}.${info.path.key}` : String(info.path.key)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Parse GraphQL query to extract accessed fields
|
|
18
|
-
const extractAccessedFields = (query: string): string[] => {
|
|
19
|
-
const fields: string[] = []
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const ast: DocumentNode = parse(query)
|
|
23
|
-
|
|
24
|
-
visit(ast, {
|
|
25
|
-
Field: {
|
|
26
|
-
enter(node: FieldNode) {
|
|
27
|
-
if (node.name.value !== '__typename') {
|
|
28
|
-
fields.push(node.name.value)
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
})
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error('Failed to parse GraphQL query:', error)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return fields
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if ability allows the operation
|
|
41
|
-
const checkPermission = (context: Context, action: string, subjectName: string, conditions?: any): boolean => {
|
|
42
|
-
if (!context.ability) {
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
// Only use subject helper if we have specific conditions to check
|
|
48
|
-
// (not just resolver args, but actual CASL conditions)
|
|
49
|
-
if (conditions && typeof conditions === 'object') {
|
|
50
|
-
return context.ability.can(action, subject(subjectName, conditions))
|
|
51
|
-
} else {
|
|
52
|
-
// Simple check without conditions - just action and subject type
|
|
53
|
-
return context.ability.can(action, subjectName)
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error('Permission check failed:', error)
|
|
57
|
-
return false
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Creates CASL authorization middleware for GraphQL resolvers
|
|
63
|
-
* @param options - Configuration options for the middleware
|
|
64
|
-
* @returns Middleware function that wraps resolvers with authorization checks
|
|
65
|
-
* @public
|
|
66
|
-
*/
|
|
67
|
-
export const createCaslMiddleware = (options: MiddlewareOptions = {}) => {
|
|
68
|
-
const {
|
|
69
|
-
subjectMap = {},
|
|
70
|
-
actionMap = {
|
|
71
|
-
query: 'read',
|
|
72
|
-
mutation: 'update',
|
|
73
|
-
subscription: 'read',
|
|
74
|
-
},
|
|
75
|
-
fieldPermissions = {},
|
|
76
|
-
abilityBuilder = defaultAbilityBuilder,
|
|
77
|
-
debug = false,
|
|
78
|
-
} = options
|
|
79
|
-
|
|
80
|
-
return async (
|
|
81
|
-
resolve: ResolverFn,
|
|
82
|
-
root: any,
|
|
83
|
-
args: any,
|
|
84
|
-
context: Context,
|
|
85
|
-
info: GraphQLResolveInfo
|
|
86
|
-
): Promise<any> => {
|
|
87
|
-
// Create ability if not already in context
|
|
88
|
-
if (!context.ability) {
|
|
89
|
-
context.ability = await createAbility(context.user, abilityBuilder)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const operation = getOperationName(info)
|
|
93
|
-
const fieldPath = getFieldPath(info)
|
|
94
|
-
const action = actionMap[operation] || operation
|
|
95
|
-
|
|
96
|
-
// Get subject from map or default to type name
|
|
97
|
-
const subjectType = info.parentType.name
|
|
98
|
-
const subjectName = subjectMap[subjectType] || subjectType
|
|
99
|
-
|
|
100
|
-
if (debug) {
|
|
101
|
-
console.debug(`Checking permission: ${action} on ${subjectName} at ${fieldPath}`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Check field-level permissions if defined
|
|
105
|
-
if (fieldPermissions[fieldPath]) {
|
|
106
|
-
const permissions = fieldPermissions[fieldPath]
|
|
107
|
-
const allowed = permissions.some(permission =>
|
|
108
|
-
// Don't pass resolver args here - only use them if the permission defines conditions
|
|
109
|
-
checkPermission(context, permission.action, permission.subject, permission.conditions)
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
if (!allowed) {
|
|
113
|
-
throw new GraphQLError(`Access denied for field ${fieldPath}`)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check operation-level permission
|
|
118
|
-
// Don't pass resolver args as conditions - they're not CASL conditions
|
|
119
|
-
const allowed = checkPermission(context, action, subjectName)
|
|
120
|
-
|
|
121
|
-
if (!allowed) {
|
|
122
|
-
throw new GraphQLError(`Access denied for ${action} on ${subjectName}`)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Additional checks for mutations with input fields
|
|
126
|
-
if (operation === 'mutation' && args.input) {
|
|
127
|
-
const querySource = info.operation.loc?.source.body
|
|
128
|
-
|
|
129
|
-
if (querySource) {
|
|
130
|
-
const accessedFields = extractAccessedFields(querySource)
|
|
131
|
-
|
|
132
|
-
for (const field of accessedFields) {
|
|
133
|
-
// Check field-level update permissions if needed
|
|
134
|
-
const fieldAllowed = checkPermission(context, 'update', subjectName, { field })
|
|
135
|
-
|
|
136
|
-
if (!fieldAllowed && debug) {
|
|
137
|
-
console.warn(`Warning: Field ${field} update not explicitly allowed`)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Call the resolver
|
|
144
|
-
return resolve(root, args, context, info)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Utility function to combine multiple middlewares
|
|
149
|
-
export const combineMiddlewares = (...middlewares: any[]) => {
|
|
150
|
-
return (resolve: ResolverFn, root: any, args: any, context: any, info: any) => {
|
|
151
|
-
const chain = middlewares.reduceRight(
|
|
152
|
-
(next, middleware) => () => middleware(next, root, args, context, info),
|
|
153
|
-
() => resolve(root, args, context, info)
|
|
154
|
-
)
|
|
155
|
-
return chain()
|
|
156
|
-
}
|
|
157
|
-
}
|