@rangka/core 0.1.1 → 0.1.3
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 +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -25
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
createModelPermissionGuard,
|
|
4
|
-
isOwnerOnly,
|
|
5
|
-
modelHasCreatedBy,
|
|
6
|
-
} from '../model-permissions.js';
|
|
7
|
-
import { PermissionRegistry } from '../permission-registry.js';
|
|
8
|
-
import { createTestServer } from '../../__tests__/helpers.js';
|
|
9
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
10
|
-
import type { ResolvedPermissions } from '../types.js';
|
|
11
|
-
|
|
12
|
-
const mockModel: ResolvedModel = {
|
|
13
|
-
qualifiedName: 'sales.customer',
|
|
14
|
-
app: 'sales',
|
|
15
|
-
module: 'sales',
|
|
16
|
-
name: 'customer',
|
|
17
|
-
auditLog: false,
|
|
18
|
-
traits: [],
|
|
19
|
-
fields: [],
|
|
20
|
-
indexes: [],
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const mockModelWithTimestamped: ResolvedModel = {
|
|
24
|
-
qualifiedName: 'sales.order',
|
|
25
|
-
app: 'sales',
|
|
26
|
-
module: 'sales',
|
|
27
|
-
name: 'order',
|
|
28
|
-
auditLog: false,
|
|
29
|
-
traits: ['timestamped'],
|
|
30
|
-
fields: [
|
|
31
|
-
{ name: 'id', config: { type: 'string' }, provenance: { source: 'base' } },
|
|
32
|
-
{
|
|
33
|
-
name: 'created_by',
|
|
34
|
-
config: { type: 'link', model: 'core.user' },
|
|
35
|
-
provenance: { source: 'trait', trait: 'timestamped' },
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'updated_by',
|
|
39
|
-
config: { type: 'link', model: 'core.user' },
|
|
40
|
-
provenance: { source: 'trait', trait: 'timestamped' },
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'created_at',
|
|
44
|
-
config: { type: 'datetime' },
|
|
45
|
-
provenance: { source: 'trait', trait: 'timestamped' },
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'updated_at',
|
|
49
|
-
config: { type: 'datetime' },
|
|
50
|
-
provenance: { source: 'trait', trait: 'timestamped' },
|
|
51
|
-
},
|
|
52
|
-
] as ResolvedModel['fields'],
|
|
53
|
-
indexes: [],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function setupServer(permissions: any) {
|
|
57
|
-
const registry = new PermissionRegistry();
|
|
58
|
-
const server = createTestServer();
|
|
59
|
-
|
|
60
|
-
server.get('/test', {
|
|
61
|
-
preHandler: createModelPermissionGuard(mockModel, registry),
|
|
62
|
-
handler: async () => ({ ok: true }),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
server.addHook('onRequest', async (request) => {
|
|
66
|
-
(request as any).authContext = { permissions };
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
return server;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
describe('model permission hook', () => {
|
|
73
|
-
it('allows access when permission is granted', async () => {
|
|
74
|
-
const server = setupServer({
|
|
75
|
-
models: { 'sales.customer': { read: true } },
|
|
76
|
-
scopes: [],
|
|
77
|
-
version: 1,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const res = await server.inject({ method: 'GET', url: '/test' });
|
|
81
|
-
expect(res.statusCode).toBe(200);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('returns 403 when permission is not granted', async () => {
|
|
85
|
-
const server = setupServer({
|
|
86
|
-
models: { 'sales.customer': { write: true } },
|
|
87
|
-
scopes: [],
|
|
88
|
-
version: 1,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const res = await server.inject({ method: 'GET', url: '/test' });
|
|
92
|
-
expect(res.statusCode).toBe(403);
|
|
93
|
-
expect(res.json().error.message).toContain('read');
|
|
94
|
-
expect(res.json().error.message).toContain('sales.customer');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('returns 403 when no permissions resolved', async () => {
|
|
98
|
-
const server = setupServer(undefined);
|
|
99
|
-
|
|
100
|
-
const res = await server.inject({ method: 'GET', url: '/test' });
|
|
101
|
-
expect(res.statusCode).toBe(403);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('checks create permission for POST', async () => {
|
|
105
|
-
const registry = new PermissionRegistry();
|
|
106
|
-
const server = createTestServer();
|
|
107
|
-
|
|
108
|
-
server.post('/test', {
|
|
109
|
-
preHandler: createModelPermissionGuard(mockModel, registry),
|
|
110
|
-
handler: async () => ({ ok: true }),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
server.addHook('onRequest', async (request) => {
|
|
114
|
-
(request as any).authContext = {
|
|
115
|
-
permissions: {
|
|
116
|
-
models: { 'sales.customer': { read: true, write: true } },
|
|
117
|
-
scopes: [],
|
|
118
|
-
version: 1,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const res = await server.inject({ method: 'POST', url: '/test', payload: {} });
|
|
124
|
-
expect(res.statusCode).toBe(403);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('allows multi-role union: if one role grants access', async () => {
|
|
128
|
-
const server = setupServer({
|
|
129
|
-
models: { 'sales.customer': { read: true, write: true, delete: true } },
|
|
130
|
-
scopes: [],
|
|
131
|
-
version: 1,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const res = await server.inject({ method: 'GET', url: '/test' });
|
|
135
|
-
expect(res.statusCode).toBe(200);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('allows access through pre-handler when permission is own', async () => {
|
|
139
|
-
const server = setupServer({
|
|
140
|
-
models: { 'sales.customer': { read: 'own' } },
|
|
141
|
-
scopes: [],
|
|
142
|
-
version: 1,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const res = await server.inject({ method: 'GET', url: '/test' });
|
|
146
|
-
expect(res.statusCode).toBe(200);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('isOwnerOnly', () => {
|
|
151
|
-
const permsWithOwn: ResolvedPermissions = {
|
|
152
|
-
models: {
|
|
153
|
-
'sales.order': { read: 'own', write: 'own', delete: 'own', create: true },
|
|
154
|
-
},
|
|
155
|
-
pages: [],
|
|
156
|
-
version: 1,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const permsWithTrue: ResolvedPermissions = {
|
|
160
|
-
models: {
|
|
161
|
-
'sales.order': { read: true, write: true, delete: true, create: true },
|
|
162
|
-
},
|
|
163
|
-
pages: [],
|
|
164
|
-
version: 1,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
it('returns true when permission is own', () => {
|
|
168
|
-
expect(isOwnerOnly(permsWithOwn, 'sales.order', 'read')).toBe(true);
|
|
169
|
-
expect(isOwnerOnly(permsWithOwn, 'sales.order', 'write')).toBe(true);
|
|
170
|
-
expect(isOwnerOnly(permsWithOwn, 'sales.order', 'delete')).toBe(true);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('returns false when permission is true', () => {
|
|
174
|
-
expect(isOwnerOnly(permsWithTrue, 'sales.order', 'read')).toBe(false);
|
|
175
|
-
expect(isOwnerOnly(permsWithTrue, 'sales.order', 'write')).toBe(false);
|
|
176
|
-
expect(isOwnerOnly(permsWithTrue, 'sales.order', 'delete')).toBe(false);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('returns false when permissions are undefined', () => {
|
|
180
|
-
expect(isOwnerOnly(undefined, 'sales.order', 'read')).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('returns false when model has no permissions', () => {
|
|
184
|
-
expect(isOwnerOnly(permsWithOwn, 'sales.unknown', 'read')).toBe(false);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('returns false when action is unset', () => {
|
|
188
|
-
const perms: ResolvedPermissions = {
|
|
189
|
-
models: { 'sales.order': { read: 'own' } },
|
|
190
|
-
pages: [],
|
|
191
|
-
version: 1,
|
|
192
|
-
};
|
|
193
|
-
expect(isOwnerOnly(perms, 'sales.order', 'write')).toBe(false);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('modelHasCreatedBy', () => {
|
|
198
|
-
it('returns true when model has created_by field', () => {
|
|
199
|
-
expect(modelHasCreatedBy(mockModelWithTimestamped)).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('returns false when model lacks created_by field', () => {
|
|
203
|
-
expect(modelHasCreatedBy(mockModel)).toBe(false);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { hashPassword, verifyPassword } from '../password.js';
|
|
3
|
-
|
|
4
|
-
describe('password hashing', () => {
|
|
5
|
-
it('hashes and verifies a password correctly', () => {
|
|
6
|
-
const password = 'my-secure-password';
|
|
7
|
-
const hashed = hashPassword(password);
|
|
8
|
-
|
|
9
|
-
expect(hashed).toContain(':');
|
|
10
|
-
expect(verifyPassword(password, hashed)).toBe(true);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('rejects an incorrect password', () => {
|
|
14
|
-
const hashed = hashPassword('correct-password');
|
|
15
|
-
expect(verifyPassword('wrong-password', hashed)).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('produces different hashes for the same password (random salt)', () => {
|
|
19
|
-
const password = 'same-password';
|
|
20
|
-
const hash1 = hashPassword(password);
|
|
21
|
-
const hash2 = hashPassword(password);
|
|
22
|
-
expect(hash1).not.toBe(hash2);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('returns false for malformed stored hash', () => {
|
|
26
|
-
expect(verifyPassword('anything', 'nocolon')).toBe(false);
|
|
27
|
-
expect(verifyPassword('anything', '')).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
PermissionRegistry,
|
|
4
|
-
DuplicateRoleError,
|
|
5
|
-
RoleInheritanceCycleError,
|
|
6
|
-
} from '../permission-registry.js';
|
|
7
|
-
|
|
8
|
-
describe('PermissionRegistry', () => {
|
|
9
|
-
describe('registerRoles', () => {
|
|
10
|
-
it('registers roles and retrieves them', () => {
|
|
11
|
-
const registry = new PermissionRegistry();
|
|
12
|
-
registry.registerRoles(
|
|
13
|
-
{
|
|
14
|
-
sales_user: {
|
|
15
|
-
label: 'Sales User',
|
|
16
|
-
models: { 'sales.customer': { read: true, write: true } },
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
'sales',
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
const role = registry.getRole('sales_user');
|
|
23
|
-
expect(role).toBeDefined();
|
|
24
|
-
expect(role!.app).toBe('sales');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('throws DuplicateRoleError for duplicate role names', () => {
|
|
28
|
-
const registry = new PermissionRegistry();
|
|
29
|
-
registry.registerRoles({ admin: { label: 'Admin', models: {} } }, 'app1');
|
|
30
|
-
|
|
31
|
-
expect(() => {
|
|
32
|
-
registry.registerRoles({ admin: { label: 'Admin', models: {} } }, 'app2');
|
|
33
|
-
}).toThrow(DuplicateRoleError);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('getAllRoles returns all registered roles', () => {
|
|
37
|
-
const registry = new PermissionRegistry();
|
|
38
|
-
registry.registerRoles(
|
|
39
|
-
{
|
|
40
|
-
role_a: { label: 'Role A', models: {} },
|
|
41
|
-
role_b: { label: 'Role B', models: {} },
|
|
42
|
-
},
|
|
43
|
-
'app1',
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
expect(registry.getAllRoles()).toHaveLength(2);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('inheritance resolution', () => {
|
|
51
|
-
it('resolves single-level inheritance', () => {
|
|
52
|
-
const registry = new PermissionRegistry();
|
|
53
|
-
registry.registerRoles(
|
|
54
|
-
{
|
|
55
|
-
parent: { label: 'Parent', models: { 'a.b': { read: true } } },
|
|
56
|
-
child: { label: 'Child', extends: 'parent', models: { 'a.b': { write: true } } },
|
|
57
|
-
},
|
|
58
|
-
'app',
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const chain = registry.getInheritanceChain('child');
|
|
62
|
-
expect(chain).toEqual(['parent']);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('resolves multi-level inheritance', () => {
|
|
66
|
-
const registry = new PermissionRegistry();
|
|
67
|
-
registry.registerRoles(
|
|
68
|
-
{
|
|
69
|
-
grandparent: { label: 'Grandparent', models: { 'a.b': { read: true } } },
|
|
70
|
-
parent: { label: 'Parent', extends: 'grandparent', models: {} },
|
|
71
|
-
child: { label: 'Child', extends: 'parent', models: {} },
|
|
72
|
-
},
|
|
73
|
-
'app',
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const chain = registry.getInheritanceChain('child');
|
|
77
|
-
expect(chain).toEqual(['grandparent', 'parent']);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('detects circular inheritance', () => {
|
|
81
|
-
const registry = new PermissionRegistry();
|
|
82
|
-
expect(() => {
|
|
83
|
-
registry.registerRoles(
|
|
84
|
-
{
|
|
85
|
-
a: { label: 'A', extends: 'b', models: {} },
|
|
86
|
-
b: { label: 'B', extends: 'a', models: {} },
|
|
87
|
-
},
|
|
88
|
-
'app',
|
|
89
|
-
);
|
|
90
|
-
}).toThrow(RoleInheritanceCycleError);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('throws for inheritance from unknown role', () => {
|
|
94
|
-
const registry = new PermissionRegistry();
|
|
95
|
-
expect(() => {
|
|
96
|
-
registry.registerRoles(
|
|
97
|
-
{
|
|
98
|
-
child: { label: 'Child', extends: 'non_existent', models: {} },
|
|
99
|
-
},
|
|
100
|
-
'app',
|
|
101
|
-
);
|
|
102
|
-
}).toThrow(/unknown role/);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('resolvePermissionsForRoles', () => {
|
|
107
|
-
it('merges permissions additively across roles', () => {
|
|
108
|
-
const registry = new PermissionRegistry();
|
|
109
|
-
registry.registerRoles(
|
|
110
|
-
{
|
|
111
|
-
role_a: { label: 'Role A', models: { 'sales.customer': { read: true, write: true } } },
|
|
112
|
-
role_b: { label: 'Role B', models: { 'sales.customer': { read: true, delete: true } } },
|
|
113
|
-
},
|
|
114
|
-
'app',
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const perms = registry.resolvePermissionsForRoles(['role_a', 'role_b']);
|
|
118
|
-
expect(perms.models['sales.customer']).toEqual({
|
|
119
|
-
read: true,
|
|
120
|
-
write: true,
|
|
121
|
-
delete: true,
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('inherits permissions from parent roles', () => {
|
|
126
|
-
const registry = new PermissionRegistry();
|
|
127
|
-
registry.registerRoles(
|
|
128
|
-
{
|
|
129
|
-
parent: {
|
|
130
|
-
label: 'Parent',
|
|
131
|
-
models: { 'sales.customer': { read: true, write: true, create: true } },
|
|
132
|
-
},
|
|
133
|
-
child: {
|
|
134
|
-
label: 'Child',
|
|
135
|
-
extends: 'parent',
|
|
136
|
-
models: { 'sales.customer': { delete: true } },
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
'app',
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const perms = registry.resolvePermissionsForRoles(['child']);
|
|
143
|
-
expect(perms.models['sales.customer']).toEqual({
|
|
144
|
-
read: true,
|
|
145
|
-
write: true,
|
|
146
|
-
create: true,
|
|
147
|
-
delete: true,
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('merges field permissions additively (most permissive wins)', () => {
|
|
152
|
-
const registry = new PermissionRegistry();
|
|
153
|
-
registry.registerRoles(
|
|
154
|
-
{
|
|
155
|
-
role_a: {
|
|
156
|
-
label: 'Role A',
|
|
157
|
-
models: {
|
|
158
|
-
'sales.invoice': {
|
|
159
|
-
read: true,
|
|
160
|
-
fieldPermissions: { cost_price: { read: false } },
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
role_b: {
|
|
165
|
-
label: 'Role B',
|
|
166
|
-
models: {
|
|
167
|
-
'sales.invoice': {
|
|
168
|
-
read: true,
|
|
169
|
-
fieldPermissions: { cost_price: { read: true, write: true } },
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
'app',
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const perms = registry.resolvePermissionsForRoles(['role_a', 'role_b']);
|
|
178
|
-
expect(perms.models['sales.invoice'].fieldPermissions!.cost_price).toEqual({
|
|
179
|
-
read: true,
|
|
180
|
-
write: true,
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('resolves pages from a single role', () => {
|
|
185
|
-
const registry = new PermissionRegistry();
|
|
186
|
-
registry.registerRoles(
|
|
187
|
-
{
|
|
188
|
-
admin: { label: 'Admin', models: {}, pages: ['admin.dashboard', 'admin.settings'] },
|
|
189
|
-
},
|
|
190
|
-
'app',
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const perms = registry.resolvePermissionsForRoles(['admin']);
|
|
194
|
-
expect(perms.pages.sort()).toEqual(['admin.dashboard', 'admin.settings']);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('merges pages from multiple roles with deduplication', () => {
|
|
198
|
-
const registry = new PermissionRegistry();
|
|
199
|
-
registry.registerRoles(
|
|
200
|
-
{
|
|
201
|
-
admin: { label: 'Admin', models: {}, pages: ['admin.dashboard', 'shared.reports'] },
|
|
202
|
-
sales: { label: 'Sales', models: {}, pages: ['sales.dashboard', 'shared.reports'] },
|
|
203
|
-
},
|
|
204
|
-
'app',
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const perms = registry.resolvePermissionsForRoles(['admin', 'sales']);
|
|
208
|
-
expect(perms.pages.sort()).toEqual(['admin.dashboard', 'sales.dashboard', 'shared.reports']);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('inherits pages from parent roles', () => {
|
|
212
|
-
const registry = new PermissionRegistry();
|
|
213
|
-
registry.registerRoles(
|
|
214
|
-
{
|
|
215
|
-
sales_rep: { label: 'Sales Rep', models: {}, pages: ['sales.dashboard'] },
|
|
216
|
-
sales_mgr: {
|
|
217
|
-
label: 'Sales Mgr',
|
|
218
|
-
extends: 'sales_rep',
|
|
219
|
-
models: {},
|
|
220
|
-
pages: ['sales.reports'],
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
'app',
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
const perms = registry.resolvePermissionsForRoles(['sales_mgr']);
|
|
227
|
-
expect(perms.pages.sort()).toEqual(['sales.dashboard', 'sales.reports']);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('returns empty pages when no role defines pages', () => {
|
|
231
|
-
const registry = new PermissionRegistry();
|
|
232
|
-
registry.registerRoles(
|
|
233
|
-
{
|
|
234
|
-
basic: { label: 'Basic', models: { 'a.b': { read: true } } },
|
|
235
|
-
},
|
|
236
|
-
'app',
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
const perms = registry.resolvePermissionsForRoles(['basic']);
|
|
240
|
-
expect(perms.pages).toEqual([]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('true permission is never downgraded by own', () => {
|
|
244
|
-
const registry = new PermissionRegistry();
|
|
245
|
-
registry.registerRoles(
|
|
246
|
-
{
|
|
247
|
-
admin: { label: 'Admin', models: { 'sales.order': { write: true, delete: true } } },
|
|
248
|
-
restricted: {
|
|
249
|
-
label: 'Restricted',
|
|
250
|
-
models: { 'sales.order': { write: 'own', delete: 'own' } },
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
'app',
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
const perms = registry.resolvePermissionsForRoles(['admin', 'restricted']);
|
|
257
|
-
expect(perms.models['sales.order'].write).toBe(true);
|
|
258
|
-
expect(perms.models['sales.order'].delete).toBe(true);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('own is upgraded to true when a more permissive role is applied', () => {
|
|
262
|
-
const registry = new PermissionRegistry();
|
|
263
|
-
registry.registerRoles(
|
|
264
|
-
{
|
|
265
|
-
restricted: {
|
|
266
|
-
label: 'Restricted',
|
|
267
|
-
models: { 'sales.order': { write: 'own', delete: 'own' } },
|
|
268
|
-
},
|
|
269
|
-
admin: { label: 'Admin', models: { 'sales.order': { write: true, delete: true } } },
|
|
270
|
-
},
|
|
271
|
-
'app',
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
const perms = registry.resolvePermissionsForRoles(['restricted', 'admin']);
|
|
275
|
-
expect(perms.models['sales.order'].write).toBe(true);
|
|
276
|
-
expect(perms.models['sales.order'].delete).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('own remains own when no higher grant exists', () => {
|
|
280
|
-
const registry = new PermissionRegistry();
|
|
281
|
-
registry.registerRoles(
|
|
282
|
-
{
|
|
283
|
-
sales_rep: {
|
|
284
|
-
label: 'Sales Rep',
|
|
285
|
-
models: { 'sales.order': { read: true, write: 'own', delete: 'own', create: true } },
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
'app',
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const perms = registry.resolvePermissionsForRoles(['sales_rep']);
|
|
292
|
-
expect(perms.models['sales.order'].write).toBe('own');
|
|
293
|
-
expect(perms.models['sales.order'].delete).toBe('own');
|
|
294
|
-
expect(perms.models['sales.order'].read).toBe(true);
|
|
295
|
-
expect(perms.models['sales.order'].create).toBe(true);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('inherited role with true overrides child own', () => {
|
|
299
|
-
const registry = new PermissionRegistry();
|
|
300
|
-
registry.registerRoles(
|
|
301
|
-
{
|
|
302
|
-
base: { label: 'Base', models: { 'sales.order': { write: true } } },
|
|
303
|
-
child: { label: 'Child', extends: 'base', models: { 'sales.order': { write: 'own' } } },
|
|
304
|
-
},
|
|
305
|
-
'app',
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
// Parent grants true first, child's 'own' should not downgrade
|
|
309
|
-
const perms = registry.resolvePermissionsForRoles(['child']);
|
|
310
|
-
expect(perms.models['sales.order'].write).toBe(true);
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
});
|