@stonecrop/casl-middleware 0.11.0 → 0.11.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/package.json +1 -1
- package/dist/src/index.js +0 -3
- package/dist/src/middleware/ability.js +0 -139
- package/dist/src/middleware/graphql.js +0 -120
- package/dist/src/middleware/introspection.js +0 -169
- package/dist/src/middleware/jwt.js +0 -291
- package/dist/src/middleware/postgraphile.js +0 -80
- package/dist/src/middleware/yoga.js +0 -32
- package/dist/src/tsdoc-metadata.json +0 -11
- package/dist/src/types/index.js +0 -0
- package/dist/tests/ability.test.d.ts +0 -2
- package/dist/tests/ability.test.d.ts.map +0 -1
- package/dist/tests/ability.test.js +0 -402
- package/dist/tests/helpers/test-utils.d.ts +0 -46
- package/dist/tests/helpers/test-utils.d.ts.map +0 -1
- package/dist/tests/helpers/test-utils.js +0 -92
- package/dist/tests/introspection.test.d.ts +0 -2
- package/dist/tests/introspection.test.d.ts.map +0 -1
- package/dist/tests/introspection.test.js +0 -368
- package/dist/tests/jwt.test.d.ts +0 -2
- package/dist/tests/jwt.test.d.ts.map +0 -1
- package/dist/tests/jwt.test.js +0 -700
- package/dist/tests/middleware.test.d.ts +0 -2
- package/dist/tests/middleware.test.d.ts.map +0 -1
- package/dist/tests/middleware.test.js +0 -184
- package/dist/tests/postgraphile-plugin.test.d.ts +0 -2
- package/dist/tests/postgraphile-plugin.test.d.ts.map +0 -1
- package/dist/tests/postgraphile-plugin.test.js +0 -56
- package/dist/tests/setup.d.ts +0 -2
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -11
- package/dist/tests/types.test.d.ts +0 -2
- package/dist/tests/types.test.d.ts.map +0 -1
- package/dist/tests/types.test.js +0 -143
- package/dist/tests/user-roles.test.d.ts +0 -2
- package/dist/tests/user-roles.test.d.ts.map +0 -1
- package/dist/tests/user-roles.test.js +0 -157
- package/dist/tests/yoga-plugin.test.d.ts +0 -2
- package/dist/tests/yoga-plugin.test.d.ts.map +0 -1
- package/dist/tests/yoga-plugin.test.js +0 -125
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.test.d.ts","sourceRoot":"","sources":["../../tests/middleware.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { PureAbility, AbilityBuilder } from '@casl/ability';
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { createCaslMiddleware } from '../src/middleware/graphql';
|
|
4
|
-
const Ability = PureAbility;
|
|
5
|
-
// Helper to create abilities with proper matchers
|
|
6
|
-
const createTestAbility = (rules) => {
|
|
7
|
-
const { can, cannot, build } = new AbilityBuilder(Ability);
|
|
8
|
-
rules.forEach(rule => {
|
|
9
|
-
if (rule.inverted) {
|
|
10
|
-
if (rule.fields && rule.conditions) {
|
|
11
|
-
cannot(rule.action, rule.subject, rule.fields, rule.conditions);
|
|
12
|
-
}
|
|
13
|
-
else if (rule.fields) {
|
|
14
|
-
cannot(rule.action, rule.subject, rule.fields);
|
|
15
|
-
}
|
|
16
|
-
else if (rule.conditions) {
|
|
17
|
-
cannot(rule.action, rule.subject, rule.conditions);
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
cannot(rule.action, rule.subject);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
if (rule.fields && rule.conditions) {
|
|
25
|
-
can(rule.action, rule.subject, rule.fields, rule.conditions);
|
|
26
|
-
}
|
|
27
|
-
else if (rule.fields) {
|
|
28
|
-
can(rule.action, rule.subject, rule.fields);
|
|
29
|
-
}
|
|
30
|
-
else if (rule.conditions) {
|
|
31
|
-
can(rule.action, rule.subject, rule.conditions);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
can(rule.action, rule.subject);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
return build({
|
|
39
|
-
detectSubjectType: (object) => object?.type || object?.constructor?.name || 'Unknown',
|
|
40
|
-
fieldMatcher: fields => field => {
|
|
41
|
-
if (!fields || !field)
|
|
42
|
-
return false;
|
|
43
|
-
return Array.isArray(fields) ? fields.includes(field) : fields === field;
|
|
44
|
-
},
|
|
45
|
-
conditionsMatcher: conditions => object => {
|
|
46
|
-
if (!conditions || !object)
|
|
47
|
-
return true;
|
|
48
|
-
return Object.keys(conditions).every(key => object[key] === conditions[key]);
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
describe('CASL GraphQL Middleware', () => {
|
|
53
|
-
let mockResolve;
|
|
54
|
-
let mockContext;
|
|
55
|
-
let mockInfo;
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
mockResolve = vi.fn().mockResolvedValue({ data: 'test' });
|
|
58
|
-
mockContext = {
|
|
59
|
-
ability: createTestAbility([
|
|
60
|
-
{ action: 'read', subject: 'Query' },
|
|
61
|
-
{ action: 'read', subject: 'User' },
|
|
62
|
-
{ action: 'update', subject: 'User', conditions: { id: '123' } },
|
|
63
|
-
]),
|
|
64
|
-
user: { id: '123', roles: ['user'] },
|
|
65
|
-
};
|
|
66
|
-
mockInfo = {
|
|
67
|
-
fieldName: 'getUser',
|
|
68
|
-
parentType: { name: 'Query' },
|
|
69
|
-
path: { typename: 'Query', key: 'getUser' },
|
|
70
|
-
operation: {
|
|
71
|
-
operation: 'query',
|
|
72
|
-
loc: { source: { body: 'query { getUser { id } }' } },
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
});
|
|
76
|
-
describe('Basic Middleware Functionality', () => {
|
|
77
|
-
it('should pass through when ability allows the operation', async () => {
|
|
78
|
-
const middleware = createCaslMiddleware();
|
|
79
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
80
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
81
|
-
expect(result).toEqual({ data: 'test' });
|
|
82
|
-
});
|
|
83
|
-
it('should create ability when not in context', async () => {
|
|
84
|
-
const middleware = createCaslMiddleware();
|
|
85
|
-
const contextWithoutAbility = { user: { id: '123', roles: ['user'] } };
|
|
86
|
-
// The middleware creates an ability if none exists
|
|
87
|
-
const result = await middleware(mockResolve, {}, {}, contextWithoutAbility, mockInfo);
|
|
88
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
89
|
-
expect(result).toEqual({ data: 'test' });
|
|
90
|
-
expect(contextWithoutAbility.ability).toBeDefined();
|
|
91
|
-
});
|
|
92
|
-
it('should deny access when ability does not allow operation', async () => {
|
|
93
|
-
const middleware = createCaslMiddleware();
|
|
94
|
-
mockContext.ability = createTestAbility([{ action: 'read', subject: 'Post' }]);
|
|
95
|
-
await expect(middleware(mockResolve, {}, {}, mockContext, mockInfo)).rejects.toThrow('Access denied for read on Query');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
describe('Subject Mapping', () => {
|
|
99
|
-
it('should map GraphQL types to custom subjects', async () => {
|
|
100
|
-
const middleware = createCaslMiddleware({
|
|
101
|
-
subjectMap: {
|
|
102
|
-
Query: 'query_operations',
|
|
103
|
-
User: 'app_user',
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
mockContext.ability = createTestAbility([{ action: 'read', subject: 'query_operations' }]);
|
|
107
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
108
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
109
|
-
expect(result).toEqual({ data: 'test' });
|
|
110
|
-
});
|
|
111
|
-
it('should use original type name when no mapping exists', async () => {
|
|
112
|
-
const middleware = createCaslMiddleware({
|
|
113
|
-
subjectMap: { User: 'app_user' },
|
|
114
|
-
});
|
|
115
|
-
// Should work with original 'Query' subject since it's not mapped
|
|
116
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
117
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
describe('Action Mapping', () => {
|
|
121
|
-
it('should map GraphQL operations to custom actions', async () => {
|
|
122
|
-
const middleware = createCaslMiddleware({
|
|
123
|
-
actionMap: {
|
|
124
|
-
query: 'view',
|
|
125
|
-
mutation: 'modify',
|
|
126
|
-
subscription: 'watch',
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
mockContext.ability = createTestAbility([{ action: 'view', subject: 'Query' }]);
|
|
130
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
131
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
describe('Field-Level Permissions', () => {
|
|
135
|
-
it('should check field-level permissions when configured', async () => {
|
|
136
|
-
const middleware = createCaslMiddleware({
|
|
137
|
-
fieldPermissions: {
|
|
138
|
-
'User.email': [{ action: 'read', subject: 'UserEmail' }],
|
|
139
|
-
'User.password': [{ action: 'read', subject: 'UserPassword' }],
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
mockInfo.parentType = { name: 'User' };
|
|
143
|
-
mockInfo.fieldName = 'email';
|
|
144
|
-
mockInfo.path = { typename: 'User', key: 'email' };
|
|
145
|
-
mockContext.ability = createTestAbility([
|
|
146
|
-
{ action: 'read', subject: 'User' },
|
|
147
|
-
{ action: 'read', subject: 'UserEmail' },
|
|
148
|
-
]);
|
|
149
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
150
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
151
|
-
});
|
|
152
|
-
it('should deny access to fields without permission', async () => {
|
|
153
|
-
const middleware = createCaslMiddleware({
|
|
154
|
-
fieldPermissions: {
|
|
155
|
-
'User.password': [{ action: 'read', subject: 'UserPassword' }],
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
mockInfo.parentType = { name: 'User' };
|
|
159
|
-
mockInfo.fieldName = 'password';
|
|
160
|
-
mockInfo.path = { typename: 'User', key: 'password' };
|
|
161
|
-
mockContext.ability = createTestAbility([{ action: 'read', subject: 'User' }]);
|
|
162
|
-
await expect(middleware(mockResolve, {}, {}, mockContext, mockInfo)).rejects.toThrow('Access denied for field User.password');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
describe('Mutation Field Checking', () => {
|
|
166
|
-
it('should check field permissions for mutations with input', async () => {
|
|
167
|
-
const middleware = createCaslMiddleware();
|
|
168
|
-
mockInfo.parentType = { name: 'Mutation' };
|
|
169
|
-
mockInfo.operation.operation = 'mutation';
|
|
170
|
-
mockInfo.operation.loc = {
|
|
171
|
-
source: {
|
|
172
|
-
body: 'mutation { updateUser(input: { name: "test", role: "admin" }) { id } }',
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
mockContext.ability = createTestAbility([
|
|
176
|
-
{ action: 'update', subject: 'Mutation' },
|
|
177
|
-
{ action: 'update', subject: 'User', fields: ['name'] },
|
|
178
|
-
]);
|
|
179
|
-
const args = { input: { name: 'test', role: 'admin' } };
|
|
180
|
-
const result = await middleware(mockResolve, {}, args, mockContext, mockInfo);
|
|
181
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postgraphile-plugin.test.d.ts","sourceRoot":"","sources":["../../tests/postgraphile-plugin.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { pglCaslPlugin } from '../src';
|
|
3
|
-
import { createConfigBasedAbilityBuilder } from '../src/middleware/ability';
|
|
4
|
-
describe('PostGraphile CASL Plugins', () => {
|
|
5
|
-
describe('pglCaslPlugin (extendSchema)', () => {
|
|
6
|
-
it('should be a valid extendSchema plugin object', () => {
|
|
7
|
-
expect(pglCaslPlugin).toBeDefined();
|
|
8
|
-
expect(pglCaslPlugin).toBeInstanceOf(Object); // It's an object, not a function
|
|
9
|
-
expect(pglCaslPlugin).toHaveProperty('name');
|
|
10
|
-
expect(pglCaslPlugin).toHaveProperty('version');
|
|
11
|
-
});
|
|
12
|
-
it('should have schema extension properties', () => {
|
|
13
|
-
// The plugin object has specific properties
|
|
14
|
-
expect(pglCaslPlugin).toHaveProperty('name');
|
|
15
|
-
expect(pglCaslPlugin.name).toMatch(/^ExtendSchemaPlugin_/);
|
|
16
|
-
// Check for schema extension function
|
|
17
|
-
expect(pglCaslPlugin).toHaveProperty('schema');
|
|
18
|
-
expect(typeof pglCaslPlugin.schema).toBe('object');
|
|
19
|
-
// Check for hooks if they exist
|
|
20
|
-
if (pglCaslPlugin.schema?.hooks) {
|
|
21
|
-
expect(pglCaslPlugin.schema.hooks).toHaveProperty('build');
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
it('should define schema extensions when executed', () => {
|
|
25
|
-
// Since extendSchema creates a plugin object, we need to test differently
|
|
26
|
-
// The actual schema extension happens when PostGraphile processes the plugin
|
|
27
|
-
// We can verify the plugin has the expected structure
|
|
28
|
-
expect(pglCaslPlugin).toHaveProperty('name');
|
|
29
|
-
expect(pglCaslPlugin).toHaveProperty('version');
|
|
30
|
-
// The schema extension function is wrapped inside the plugin
|
|
31
|
-
// and will be executed by PostGraphile
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('Ability Integration', () => {
|
|
35
|
-
it('should create abilities with custom builder', async () => {
|
|
36
|
-
const customBuilder = createConfigBasedAbilityBuilder({
|
|
37
|
-
defaultRules: [{ action: 'read', subject: 'Query' }],
|
|
38
|
-
roles: {
|
|
39
|
-
editor: [
|
|
40
|
-
{ action: 'create', subject: 'Post' },
|
|
41
|
-
{ action: 'update', subject: 'Post' },
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
// Test that the ability builder works
|
|
46
|
-
const ability = await customBuilder({ id: '123', roles: ['editor'] });
|
|
47
|
-
expect(ability.can('create', 'Post')).toBe(true);
|
|
48
|
-
expect(ability.can('update', 'Post')).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
it('should work with PostGraphile plugin system', () => {
|
|
51
|
-
// Test that our plugin has the correct shape for PostGraphile
|
|
52
|
-
expect(pglCaslPlugin).toHaveProperty('name');
|
|
53
|
-
expect(pglCaslPlugin).toHaveProperty('version');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
package/dist/tests/setup.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../tests/setup.ts"],"names":[],"mappings":""}
|
package/dist/tests/setup.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.test.d.ts","sourceRoot":"","sources":["../../tests/types.test.ts"],"names":[],"mappings":""}
|
package/dist/tests/types.test.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it, expectTypeOf } from 'vitest';
|
|
2
|
-
import { createAbility, detectSubjectType, defaultAbilityBuilder, createDatabaseAbilityBuilder, createConfigBasedAbilityBuilder, } from '../src/middleware/ability';
|
|
3
|
-
import { createJWTMiddleware, createJWT } from '../src/middleware/jwt';
|
|
4
|
-
import { createCaslMiddleware } from '../src/middleware/graphql';
|
|
5
|
-
describe('Type Safety Tests', () => {
|
|
6
|
-
describe('Ability Types', () => {
|
|
7
|
-
it('should have correct AppAbility type structure', () => {
|
|
8
|
-
expectTypeOf().toExtend(); // AppAbility extends PureAbility base type
|
|
9
|
-
});
|
|
10
|
-
it('should infer correct return type for createAbility', async () => {
|
|
11
|
-
const ability = await createAbility({ id: '1', roles: ['user'] });
|
|
12
|
-
expectTypeOf(ability).toEqualTypeOf();
|
|
13
|
-
});
|
|
14
|
-
it('should accept AbilityBuilderFunction as parameter', () => {
|
|
15
|
-
const builder = user => defaultAbilityBuilder(user);
|
|
16
|
-
expectTypeOf(builder).parameter(0).toEqualTypeOf();
|
|
17
|
-
expectTypeOf(builder).returns.resolves.toEqualTypeOf();
|
|
18
|
-
});
|
|
19
|
-
it('should have correct detectSubjectType signature', () => {
|
|
20
|
-
expectTypeOf(detectSubjectType).parameter(0).toEqualTypeOf();
|
|
21
|
-
expectTypeOf(detectSubjectType).returns.toEqualTypeOf();
|
|
22
|
-
});
|
|
23
|
-
it('should type database ability builder correctly', () => {
|
|
24
|
-
const fetchRules = async (userId) => [];
|
|
25
|
-
const builder = createDatabaseAbilityBuilder(fetchRules);
|
|
26
|
-
expectTypeOf(builder).parameter(0).toEqualTypeOf();
|
|
27
|
-
expectTypeOf(builder).returns.resolves.toEqualTypeOf();
|
|
28
|
-
});
|
|
29
|
-
it('should type config-based ability builder correctly', () => {
|
|
30
|
-
const config = {
|
|
31
|
-
roles: {
|
|
32
|
-
admin: [{ action: 'manage', subject: 'all' }],
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
const builder = createConfigBasedAbilityBuilder(config);
|
|
36
|
-
expectTypeOf(builder).parameter(0).toEqualTypeOf();
|
|
37
|
-
// Builder returns AppAbility synchronously
|
|
38
|
-
expectTypeOf(builder).toBeFunction();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
describe('JWT Types', () => {
|
|
42
|
-
it('should have correct JWTConfig type structure', () => {
|
|
43
|
-
const config = {
|
|
44
|
-
secret: 'test',
|
|
45
|
-
enabled: true,
|
|
46
|
-
optional: false,
|
|
47
|
-
};
|
|
48
|
-
expectTypeOf(config).toExtend(); // Check config has at least these properties
|
|
49
|
-
});
|
|
50
|
-
it('should have correct JWTPayload structure', () => {
|
|
51
|
-
expectTypeOf().toHaveProperty('sub');
|
|
52
|
-
expectTypeOf().toHaveProperty('iat');
|
|
53
|
-
});
|
|
54
|
-
it('should infer correct return type for createJWT', () => {
|
|
55
|
-
const user = { id: '1', roles: ['user'] };
|
|
56
|
-
const token = createJWT(user, { secret: 'test' });
|
|
57
|
-
expectTypeOf(token).toBeString();
|
|
58
|
-
});
|
|
59
|
-
it('should type middleware correctly', () => {
|
|
60
|
-
const middleware = createJWTMiddleware({ secret: 'test' });
|
|
61
|
-
expectTypeOf(middleware).parameter(0).toEqualTypeOf();
|
|
62
|
-
expectTypeOf(middleware).parameter(1).toBeFunction();
|
|
63
|
-
expectTypeOf(middleware).returns.toEqualTypeOf();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe('GraphQL Middleware Types', () => {
|
|
67
|
-
it('should have correct MiddlewareOptions type structure', () => {
|
|
68
|
-
const options = {
|
|
69
|
-
subjectMap: {},
|
|
70
|
-
actionMap: {},
|
|
71
|
-
fieldPermissions: {},
|
|
72
|
-
debug: false,
|
|
73
|
-
};
|
|
74
|
-
expectTypeOf(options).toExtend(); // Check options has at least these properties
|
|
75
|
-
});
|
|
76
|
-
it('should type middleware function correctly', () => {
|
|
77
|
-
const middleware = createCaslMiddleware();
|
|
78
|
-
expectTypeOf(middleware).toBeFunction();
|
|
79
|
-
expectTypeOf(middleware).parameter(0).toBeFunction(); // resolve
|
|
80
|
-
expectTypeOf(middleware).parameter(1).toBeAny(); // root
|
|
81
|
-
expectTypeOf(middleware).parameter(2).toBeAny(); // args
|
|
82
|
-
expectTypeOf(middleware).parameter(3).toEqualTypeOf(); // context
|
|
83
|
-
expectTypeOf(middleware).returns.toEqualTypeOf();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe('Context Types', () => {
|
|
87
|
-
it('should have correct Context type structure', () => {
|
|
88
|
-
expectTypeOf().toHaveProperty('ability');
|
|
89
|
-
expectTypeOf().toHaveProperty('user');
|
|
90
|
-
});
|
|
91
|
-
it('should have correct User type structure', () => {
|
|
92
|
-
expectTypeOf().toHaveProperty('id');
|
|
93
|
-
expectTypeOf().toHaveProperty('roles');
|
|
94
|
-
const user = { id: '1', roles: ['admin'] };
|
|
95
|
-
expectTypeOf(user.id).toBeString();
|
|
96
|
-
// roles is optional, so check the array type when present
|
|
97
|
-
if (user.roles) {
|
|
98
|
-
expectTypeOf(user.roles).toEqualTypeOf();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
it('should allow optional properties on User', () => {
|
|
102
|
-
const userWithOptionals = {
|
|
103
|
-
id: '1',
|
|
104
|
-
roles: ['user'],
|
|
105
|
-
email: 'test@example.com',
|
|
106
|
-
};
|
|
107
|
-
expectTypeOf(userWithOptionals).toExtend(); // Check that object is assignable to User type
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe('Ability Rule Types', () => {
|
|
111
|
-
it('should correctly type ability actions', () => {
|
|
112
|
-
const ability = defaultAbilityBuilder({ id: '1', roles: ['user'] });
|
|
113
|
-
// Type checking that these calls are valid
|
|
114
|
-
expectTypeOf(ability.can).parameter(0).toBeString();
|
|
115
|
-
expectTypeOf(ability.can).parameter(1).toEqualTypeOf();
|
|
116
|
-
});
|
|
117
|
-
it('should support subject type checking', () => {
|
|
118
|
-
const ability = defaultAbilityBuilder({ id: '1', roles: ['user'] });
|
|
119
|
-
// Verify can() method signature accepts string action and optional subject
|
|
120
|
-
expectTypeOf(ability.can).parameter(0).toBeString();
|
|
121
|
-
expectTypeOf(ability.can).toBeCallableWith('read', 'Post');
|
|
122
|
-
expectTypeOf(ability.can).toBeCallableWith('read', undefined);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
describe('Builder Function Types', () => {
|
|
126
|
-
it('should accept sync and async builder functions', () => {
|
|
127
|
-
const syncBuilder = () => defaultAbilityBuilder();
|
|
128
|
-
const asyncBuilder = async () => defaultAbilityBuilder();
|
|
129
|
-
// Both should be valid builder functions
|
|
130
|
-
expectTypeOf(syncBuilder).toBeFunction();
|
|
131
|
-
expectTypeOf(asyncBuilder).toBeFunction();
|
|
132
|
-
expectTypeOf(asyncBuilder).returns.resolves.toEqualTypeOf();
|
|
133
|
-
});
|
|
134
|
-
it('should enforce correct parameter types for builders', () => {
|
|
135
|
-
const builder = user => {
|
|
136
|
-
expectTypeOf(user).toEqualTypeOf();
|
|
137
|
-
return defaultAbilityBuilder(user);
|
|
138
|
-
};
|
|
139
|
-
expectTypeOf(builder).toBeCallableWith(undefined);
|
|
140
|
-
expectTypeOf(builder).toBeCallableWith({ id: '1', roles: [] });
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"user-roles.test.d.ts","sourceRoot":"","sources":["../../tests/user-roles.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { PureAbility, AbilityBuilder } from '@casl/ability';
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { createCaslMiddleware } from '../src/middleware/graphql';
|
|
4
|
-
const Ability = PureAbility;
|
|
5
|
-
// Helper to create abilities with proper matchers
|
|
6
|
-
const createTestAbility = (rules) => {
|
|
7
|
-
const { can, cannot, build } = new AbilityBuilder(Ability);
|
|
8
|
-
rules.forEach(rule => {
|
|
9
|
-
if (rule.inverted) {
|
|
10
|
-
if (rule.fields && rule.conditions) {
|
|
11
|
-
cannot(rule.action, rule.subject, rule.fields, rule.conditions);
|
|
12
|
-
}
|
|
13
|
-
else if (rule.fields) {
|
|
14
|
-
cannot(rule.action, rule.subject, rule.fields);
|
|
15
|
-
}
|
|
16
|
-
else if (rule.conditions) {
|
|
17
|
-
cannot(rule.action, rule.subject, rule.conditions);
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
cannot(rule.action, rule.subject);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
if (rule.fields && rule.conditions) {
|
|
25
|
-
can(rule.action, rule.subject, rule.fields, rule.conditions);
|
|
26
|
-
}
|
|
27
|
-
else if (rule.fields) {
|
|
28
|
-
can(rule.action, rule.subject, rule.fields);
|
|
29
|
-
}
|
|
30
|
-
else if (rule.conditions) {
|
|
31
|
-
can(rule.action, rule.subject, rule.conditions);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
can(rule.action, rule.subject);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
return build({
|
|
39
|
-
detectSubjectType: (object) => {
|
|
40
|
-
// Handle CASL's subject() helper objects
|
|
41
|
-
if (object && typeof object === 'object') {
|
|
42
|
-
// CASL uses __caslSubjectType__ property
|
|
43
|
-
if ('__caslSubjectType__' in object) {
|
|
44
|
-
return object.__caslSubjectType__;
|
|
45
|
-
}
|
|
46
|
-
// Fallback to other type indicators
|
|
47
|
-
if (object.type)
|
|
48
|
-
return object.type;
|
|
49
|
-
if (object.constructor?.name && object.constructor.name !== 'Object') {
|
|
50
|
-
return object.constructor.name;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return 'Unknown';
|
|
54
|
-
},
|
|
55
|
-
fieldMatcher: fields => field => {
|
|
56
|
-
if (!fields || !field)
|
|
57
|
-
return false;
|
|
58
|
-
return Array.isArray(fields) ? fields.includes(field) : fields === field;
|
|
59
|
-
},
|
|
60
|
-
conditionsMatcher: conditions => object => {
|
|
61
|
-
if (!conditions || !object)
|
|
62
|
-
return true;
|
|
63
|
-
return Object.keys(conditions).every(key => object[key] === conditions[key]);
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
describe('CASL GraphQL Middleware', () => {
|
|
68
|
-
let mockResolve;
|
|
69
|
-
let mockContext;
|
|
70
|
-
let mockInfo;
|
|
71
|
-
beforeEach(() => {
|
|
72
|
-
mockResolve = vi.fn().mockResolvedValue({ data: 'test' });
|
|
73
|
-
mockContext = {
|
|
74
|
-
ability: createTestAbility([
|
|
75
|
-
{ action: 'read', subject: 'Query' },
|
|
76
|
-
{ action: 'read', subject: 'User' },
|
|
77
|
-
{ action: 'update', subject: 'User', conditions: { id: '123' } },
|
|
78
|
-
]),
|
|
79
|
-
user: { id: '123', roles: ['user'] },
|
|
80
|
-
};
|
|
81
|
-
mockInfo = {
|
|
82
|
-
fieldName: 'getUser',
|
|
83
|
-
parentType: { name: 'Query' },
|
|
84
|
-
path: { typename: 'Query', key: 'getUser' },
|
|
85
|
-
operation: {
|
|
86
|
-
operation: 'query',
|
|
87
|
-
loc: { source: { body: 'query { getUser { id } }' } },
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
});
|
|
91
|
-
describe('Basic Middleware Functionality', () => {
|
|
92
|
-
it('should pass through when ability allows the operation', async () => {
|
|
93
|
-
const middleware = createCaslMiddleware();
|
|
94
|
-
// Don't pass args that would be treated as conditions
|
|
95
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
96
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
97
|
-
expect(result).toEqual({ data: 'test' });
|
|
98
|
-
});
|
|
99
|
-
it('should create ability when not in context', async () => {
|
|
100
|
-
const middleware = createCaslMiddleware();
|
|
101
|
-
const contextWithoutAbility = { user: { id: '123', roles: ['user'] } };
|
|
102
|
-
// The middleware creates a default ability if none exists
|
|
103
|
-
// Default ability only allows 'read' on 'Query'
|
|
104
|
-
const result = await middleware(mockResolve, {}, {}, contextWithoutAbility, mockInfo);
|
|
105
|
-
// Should work because default ability allows 'read' on 'Query'
|
|
106
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
107
|
-
expect(result).toEqual({ data: 'test' });
|
|
108
|
-
expect(contextWithoutAbility.ability).toBeDefined();
|
|
109
|
-
});
|
|
110
|
-
it('should deny access when ability does not allow operation', async () => {
|
|
111
|
-
const middleware = createCaslMiddleware();
|
|
112
|
-
// Create ability that doesn't allow 'read' on 'Query'
|
|
113
|
-
mockContext.ability = createTestAbility([{ action: 'read', subject: 'Post' }]);
|
|
114
|
-
await expect(middleware(mockResolve, {}, {}, mockContext, mockInfo)).rejects.toThrow('Access denied for read on Query');
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe('Action Mapping', () => {
|
|
118
|
-
it('should map GraphQL operations to custom actions', async () => {
|
|
119
|
-
const middleware = createCaslMiddleware({
|
|
120
|
-
actionMap: {
|
|
121
|
-
query: 'view',
|
|
122
|
-
mutation: 'modify',
|
|
123
|
-
subscription: 'watch',
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
// Update ability to allow 'view' instead of 'read'
|
|
127
|
-
mockContext.ability = createTestAbility([{ action: 'view', subject: 'Query' }]);
|
|
128
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
129
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
130
|
-
expect(result).toEqual({ data: 'test' });
|
|
131
|
-
});
|
|
132
|
-
it('should deny when custom action is not allowed', async () => {
|
|
133
|
-
const middleware = createCaslMiddleware({
|
|
134
|
-
actionMap: {
|
|
135
|
-
query: 'view',
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
// Ability still has 'read' permission, not 'view'
|
|
139
|
-
await expect(middleware(mockResolve, {}, {}, mockContext, mockInfo)).rejects.toThrow('Access denied for view on Query');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
describe('Subject Conditions', () => {
|
|
143
|
-
it('should handle conditional permissions correctly', async () => {
|
|
144
|
-
const middleware = createCaslMiddleware();
|
|
145
|
-
// Change to User type which has conditional permission
|
|
146
|
-
mockInfo.parentType = { name: 'User' };
|
|
147
|
-
mockInfo.path = { typename: 'User', key: 'updateUser' };
|
|
148
|
-
mockInfo.operation.operation = 'mutation';
|
|
149
|
-
// Ability allows update on User with id: '123'
|
|
150
|
-
mockContext.ability = createTestAbility([{ action: 'update', subject: 'User', conditions: { id: '123' } }]);
|
|
151
|
-
// This should work because we're not passing the conditions through args
|
|
152
|
-
// The middleware should check the general permission
|
|
153
|
-
const result = await middleware(mockResolve, {}, {}, mockContext, mockInfo);
|
|
154
|
-
expect(mockResolve).toHaveBeenCalled();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"yoga-plugin.test.d.ts","sourceRoot":"","sources":["../../tests/yoga-plugin.test.ts"],"names":[],"mappings":""}
|