@objectstack/plugin-security 4.0.4 → 4.1.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.
@@ -1,143 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { RowLevelSecurityPolicy } from '@objectstack/spec/security';
4
- import type { ExecutionContext } from '@objectstack/spec/kernel';
5
-
6
- /**
7
- * RLS User Context
8
- * Variables available for RLS expression evaluation.
9
- */
10
- interface RLSUserContext {
11
- id?: string;
12
- tenant_id?: string;
13
- roles?: string[];
14
- [key: string]: unknown;
15
- }
16
-
17
- /**
18
- * RLSCompiler
19
- *
20
- * Compiles Row-Level Security policy expressions into query filters.
21
- * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.
22
- */
23
- export class RLSCompiler {
24
- /**
25
- * Compile RLS policies into a query filter for the given user context.
26
- * Multiple policies for the same object/operation are OR-combined (any match allows access).
27
- */
28
- compileFilter(
29
- policies: RowLevelSecurityPolicy[],
30
- executionContext?: ExecutionContext
31
- ): Record<string, unknown> | null {
32
- if (policies.length === 0) return null;
33
-
34
- const userCtx: RLSUserContext = {
35
- id: executionContext?.userId,
36
- tenant_id: executionContext?.tenantId,
37
- roles: executionContext?.roles,
38
- };
39
-
40
- const filters: Record<string, unknown>[] = [];
41
-
42
- for (const policy of policies) {
43
- if (!policy.using) continue;
44
- const filter = this.compileExpression(policy.using, userCtx);
45
- if (filter) {
46
- filters.push(filter);
47
- }
48
- }
49
-
50
- if (filters.length === 0) return null;
51
- if (filters.length === 1) return filters[0];
52
-
53
- // Multiple policies: OR-combine (any policy allows access)
54
- return { $or: filters };
55
- }
56
-
57
- /**
58
- * Compile a single RLS expression into a query filter.
59
- *
60
- * Supports simple expressions like:
61
- * - "field_name = current_user.property"
62
- * - "field_name IN (current_user.array_property)"
63
- * - "field_name = 'literal_value'"
64
- */
65
- compileExpression(
66
- expression: string,
67
- userCtx: RLSUserContext
68
- ): Record<string, unknown> | null {
69
- if (!expression) return null;
70
-
71
- // Handle simple equality: "field = current_user.property"
72
- const eqMatch = expression.match(/^\s*(\w+)\s*=\s*current_user\.(\w+)\s*$/);
73
- if (eqMatch) {
74
- const [, field, prop] = eqMatch;
75
- const value = userCtx[prop];
76
- if (value === undefined) return null;
77
- return { [field]: value };
78
- }
79
-
80
- // Handle literal equality: "field = 'value'"
81
- const litMatch = expression.match(/^\s*(\w+)\s*=\s*'([^']*)'\s*$/);
82
- if (litMatch) {
83
- const [, field, value] = litMatch;
84
- return { [field]: value };
85
- }
86
-
87
- // Handle IN: "field IN (current_user.array_property)"
88
- const inMatch = expression.match(/^\s*(\w+)\s+IN\s+\(\s*current_user\.(\w+)\s*\)\s*$/i);
89
- if (inMatch) {
90
- const [, field, prop] = inMatch;
91
- const value = userCtx[prop];
92
- if (!Array.isArray(value)) return null;
93
- return { [field]: { $in: value } };
94
- }
95
-
96
- // Unsupported expression: return null (no additional RLS filter applied).
97
- // Note: callers should treat absence of RLS policies as "allow all" only when
98
- // no policies are defined. If policies exist but cannot be compiled, the caller
99
- // may want to deny access as a safety measure.
100
- return null;
101
- }
102
-
103
- /**
104
- * Get applicable RLS policies for a given object and operation.
105
- */
106
- getApplicablePolicies(
107
- objectName: string,
108
- operation: string,
109
- allPolicies: RowLevelSecurityPolicy[]
110
- ): RowLevelSecurityPolicy[] {
111
- // Map engine operation to RLS operation type
112
- const rlsOp = this.mapOperationToRLS(operation);
113
-
114
- return allPolicies.filter(policy => {
115
- // Check object match
116
- if (policy.object !== objectName && policy.object !== '*') return false;
117
-
118
- // Check operation match
119
- if (policy.operation === 'all') return true;
120
- if (policy.operation === rlsOp) return true;
121
-
122
- return false;
123
- });
124
- }
125
-
126
- private mapOperationToRLS(operation: string): string {
127
- switch (operation) {
128
- case 'find':
129
- case 'findOne':
130
- case 'count':
131
- case 'aggregate':
132
- return 'select';
133
- case 'insert':
134
- return 'insert';
135
- case 'update':
136
- return 'update';
137
- case 'delete':
138
- return 'delete';
139
- default:
140
- return 'select';
141
- }
142
- }
143
- }
@@ -1,302 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, vi } from 'vitest';
4
- import { SecurityPlugin } from './security-plugin.js';
5
- import { PermissionEvaluator } from './permission-evaluator.js';
6
- import { FieldMasker } from './field-masker.js';
7
- import { RLSCompiler } from './rls-compiler.js';
8
- import type { PermissionSet } from '@objectstack/spec/security';
9
-
10
- // ---------------------------------------------------------------------------
11
- // SecurityPlugin – basic metadata
12
- // ---------------------------------------------------------------------------
13
- describe('SecurityPlugin', () => {
14
- it('should have correct metadata', () => {
15
- const plugin = new SecurityPlugin();
16
- expect(plugin.name).toBe('com.objectstack.security');
17
- expect(plugin.type).toBe('standard');
18
- expect(plugin.version).toBe('1.0.0');
19
- expect(plugin.dependencies).toContain('com.objectstack.engine.objectql');
20
- });
21
-
22
- it('should register services during init', async () => {
23
- const plugin = new SecurityPlugin();
24
- const manifestService = { register: vi.fn() };
25
- const ctx: any = {
26
- logger: { info: vi.fn(), warn: vi.fn() },
27
- registerService: vi.fn(),
28
- getService: vi.fn().mockImplementation((name: string) => {
29
- if (name === 'manifest') return manifestService;
30
- return undefined;
31
- }),
32
- };
33
- await plugin.init(ctx);
34
- expect(ctx.registerService).toHaveBeenCalledWith('security.permissions', expect.any(PermissionEvaluator));
35
- expect(ctx.registerService).toHaveBeenCalledWith('security.rls', expect.any(RLSCompiler));
36
- expect(ctx.registerService).toHaveBeenCalledWith('security.fieldMasker', expect.any(FieldMasker));
37
- expect(manifestService.register).toHaveBeenCalled();
38
- });
39
-
40
- it('should warn and return when objectql service is missing', async () => {
41
- const plugin = new SecurityPlugin();
42
- const manifestService = { register: vi.fn() };
43
- const ctx: any = {
44
- logger: { info: vi.fn(), warn: vi.fn() },
45
- registerService: vi.fn(),
46
- getService: vi.fn().mockImplementation((name: string) => {
47
- if (name === 'manifest') return manifestService;
48
- throw new Error('not found');
49
- }),
50
- };
51
- await plugin.init(ctx);
52
- await plugin.start(ctx);
53
- expect(ctx.logger.warn).toHaveBeenCalled();
54
- });
55
-
56
- it('should warn when objectql does not support middleware', async () => {
57
- const plugin = new SecurityPlugin();
58
- const manifestService = { register: vi.fn() };
59
- const ctx: any = {
60
- logger: { info: vi.fn(), warn: vi.fn() },
61
- registerService: vi.fn(),
62
- getService: vi.fn().mockImplementation((name: string) => {
63
- if (name === 'manifest') return manifestService;
64
- return {}; // objectql without registerMiddleware
65
- }),
66
- };
67
- await plugin.init(ctx);
68
- await plugin.start(ctx);
69
- expect(ctx.logger.warn).toHaveBeenCalled();
70
- });
71
-
72
- it('should register middleware when objectql supports it', async () => {
73
- const plugin = new SecurityPlugin();
74
- const registerMiddleware = vi.fn();
75
- const manifestService = { register: vi.fn() };
76
- const ctx: any = {
77
- logger: { info: vi.fn(), warn: vi.fn() },
78
- registerService: vi.fn(),
79
- getService: vi.fn().mockImplementation((name: string) => {
80
- if (name === 'manifest') return manifestService;
81
- return { registerMiddleware };
82
- }),
83
- };
84
- await plugin.init(ctx);
85
- await plugin.start(ctx);
86
- expect(registerMiddleware).toHaveBeenCalledWith(expect.any(Function));
87
- });
88
-
89
- it('should destroy without error', async () => {
90
- const plugin = new SecurityPlugin();
91
- await expect(plugin.destroy()).resolves.toBeUndefined();
92
- });
93
- });
94
-
95
- // ---------------------------------------------------------------------------
96
- // PermissionEvaluator
97
- // ---------------------------------------------------------------------------
98
- describe('PermissionEvaluator', () => {
99
- const makePermSet = (
100
- name: string,
101
- objects: PermissionSet['objects'] = {},
102
- fields: PermissionSet['fields'] = {}
103
- ): PermissionSet => ({ name, objects, fields });
104
-
105
- it('should allow read when allowRead is true', () => {
106
- const evaluator = new PermissionEvaluator();
107
- const ps = makePermSet('admin', { contact: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } });
108
- expect(evaluator.checkObjectPermission('find', 'contact', [ps])).toBe(true);
109
- expect(evaluator.checkObjectPermission('findOne', 'contact', [ps])).toBe(true);
110
- expect(evaluator.checkObjectPermission('count', 'contact', [ps])).toBe(true);
111
- });
112
-
113
- it('should deny when no permission set matches', () => {
114
- const evaluator = new PermissionEvaluator();
115
- const ps = makePermSet('readonly', { contact: { allowRead: false, allowCreate: false, allowEdit: false, allowDelete: false } });
116
- expect(evaluator.checkObjectPermission('insert', 'contact', [ps])).toBe(false);
117
- });
118
-
119
- it('should allow unknown operations by default', () => {
120
- const evaluator = new PermissionEvaluator();
121
- expect(evaluator.checkObjectPermission('unknownOp', 'contact', [])).toBe(true);
122
- });
123
-
124
- it('should allow via viewAllRecords', () => {
125
- const evaluator = new PermissionEvaluator();
126
- const ps = makePermSet('viewer', { task: { allowRead: false, allowCreate: false, allowEdit: false, allowDelete: false, viewAllRecords: true } });
127
- expect(evaluator.checkObjectPermission('find', 'task', [ps])).toBe(true);
128
- });
129
-
130
- it('should allow edit/delete via modifyAllRecords', () => {
131
- const evaluator = new PermissionEvaluator();
132
- const ps = makePermSet('manager', { task: { allowRead: false, allowCreate: false, allowEdit: false, allowDelete: false, modifyAllRecords: true } });
133
- expect(evaluator.checkObjectPermission('update', 'task', [ps])).toBe(true);
134
- expect(evaluator.checkObjectPermission('delete', 'task', [ps])).toBe(true);
135
- });
136
-
137
- it('should merge field permissions (most permissive)', () => {
138
- const evaluator = new PermissionEvaluator();
139
- const ps1 = makePermSet('ps1', {}, { 'contact.email': { readable: true, editable: false } });
140
- const ps2 = makePermSet('ps2', {}, { 'contact.email': { readable: false, editable: true } });
141
- const result = evaluator.getFieldPermissions('contact', [ps1, ps2]);
142
- expect(result['email']).toEqual({ readable: true, editable: true });
143
- });
144
-
145
- it('should filter field permissions to the correct object', () => {
146
- const evaluator = new PermissionEvaluator();
147
- const ps = makePermSet('ps', {}, {
148
- 'contact.email': { readable: true, editable: false },
149
- 'task.title': { readable: true, editable: true },
150
- });
151
- const result = evaluator.getFieldPermissions('contact', [ps]);
152
- expect(result['email']).toBeDefined();
153
- expect(result['title']).toBeUndefined();
154
- });
155
-
156
- it('should resolve permission sets from metadata service by role name', () => {
157
- const evaluator = new PermissionEvaluator();
158
- const ps1 = { name: 'admin' };
159
- const ps2 = { name: 'viewer' };
160
- const metadata = { list: vi.fn().mockReturnValue([ps1, ps2]) };
161
- const result = evaluator.resolvePermissionSets(['admin'], metadata);
162
- expect(result).toEqual([ps1]);
163
- });
164
-
165
- it('should return empty array when metadata has no permission sets', () => {
166
- const evaluator = new PermissionEvaluator();
167
- const metadata = { list: vi.fn().mockReturnValue([]) };
168
- expect(evaluator.resolvePermissionSets(['admin'], metadata)).toEqual([]);
169
- });
170
- });
171
-
172
- // ---------------------------------------------------------------------------
173
- // FieldMasker
174
- // ---------------------------------------------------------------------------
175
- describe('FieldMasker', () => {
176
- it('should return results unchanged when no field permissions', () => {
177
- const masker = new FieldMasker();
178
- const records = [{ id: '1', name: 'Alice', email: 'alice@example.com' }];
179
- expect(masker.maskResults(records, {}, 'contact')).toEqual(records);
180
- });
181
-
182
- it('should remove non-readable fields from records', () => {
183
- const masker = new FieldMasker();
184
- const records = [{ id: '1', name: 'Alice', email: 'alice@example.com' }];
185
- const perms = { email: { readable: false, editable: false } };
186
- const result = masker.maskResults(records, perms, 'contact') as any[];
187
- expect(result[0].email).toBeUndefined();
188
- expect(result[0].name).toBe('Alice');
189
- });
190
-
191
- it('should handle single record (non-array)', () => {
192
- const masker = new FieldMasker();
193
- const record = { id: '1', ssn: '123-45-6789', name: 'Bob' };
194
- const perms = { ssn: { readable: false, editable: false } };
195
- const result = masker.maskResults(record, perms, 'person') as any;
196
- expect(result.ssn).toBeUndefined();
197
- expect(result.name).toBe('Bob');
198
- });
199
-
200
- it('should preserve readable fields', () => {
201
- const masker = new FieldMasker();
202
- const record = { id: '1', name: 'Carol', secret: 'x' };
203
- const perms = {
204
- name: { readable: true, editable: true },
205
- secret: { readable: false, editable: false },
206
- };
207
- const result = masker.maskResults(record, perms, 'user') as any;
208
- expect(result.name).toBe('Carol');
209
- expect(result.secret).toBeUndefined();
210
- });
211
-
212
- it('should return non-editable fields', () => {
213
- const masker = new FieldMasker();
214
- const perms = {
215
- email: { readable: true, editable: false },
216
- name: { readable: true, editable: true },
217
- };
218
- const nonEditable = masker.getNonEditableFields(perms);
219
- expect(nonEditable).toContain('email');
220
- expect(nonEditable).not.toContain('name');
221
- });
222
-
223
- it('should strip non-editable fields from write data', () => {
224
- const masker = new FieldMasker();
225
- const data = { name: 'Dave', email: 'dave@example.com', createdAt: '2024' };
226
- const perms = {
227
- email: { readable: true, editable: false },
228
- createdAt: { readable: true, editable: false },
229
- name: { readable: true, editable: true },
230
- };
231
- const result = masker.stripNonEditableFields(data, perms);
232
- expect(result.name).toBe('Dave');
233
- expect(result.email).toBeUndefined();
234
- expect(result.createdAt).toBeUndefined();
235
- });
236
- });
237
-
238
- // ---------------------------------------------------------------------------
239
- // RLSCompiler
240
- // ---------------------------------------------------------------------------
241
- describe('RLSCompiler', () => {
242
- it('should return null for empty policies', () => {
243
- const compiler = new RLSCompiler();
244
- expect(compiler.compileFilter([])).toBeNull();
245
- });
246
-
247
- it('should compile equality expression with current_user property', () => {
248
- const compiler = new RLSCompiler();
249
- const policy: any = { object: 'task', operation: 'select', using: 'owner_id = current_user.id' };
250
- const ctx: any = { userId: 'user-42', tenantId: 'tenant-1', roles: [] };
251
- const filter = compiler.compileFilter([policy], ctx);
252
- expect(filter).toEqual({ owner_id: 'user-42' });
253
- });
254
-
255
- it('should compile literal equality expression', () => {
256
- const compiler = new RLSCompiler();
257
- const policy: any = { object: 'doc', operation: 'select', using: "status = 'published'" };
258
- const filter = compiler.compileFilter([policy]);
259
- expect(filter).toEqual({ status: 'published' });
260
- });
261
-
262
- it('should compile IN expression with array property', () => {
263
- const compiler = new RLSCompiler();
264
- const policy: any = { object: 'project', operation: 'select', using: 'id IN (current_user.roles)' };
265
- const ctx: any = { userId: 'u1', tenantId: 't1', roles: ['role-a', 'role-b'] };
266
- const filter = compiler.compileFilter([policy], ctx);
267
- expect(filter).toEqual({ id: { $in: ['role-a', 'role-b'] } });
268
- });
269
-
270
- it('should OR-combine multiple policies', () => {
271
- const compiler = new RLSCompiler();
272
- const p1: any = { object: 'task', operation: 'select', using: 'owner_id = current_user.id' };
273
- const p2: any = { object: 'task', operation: 'select', using: "status = 'public'" };
274
- const ctx: any = { userId: 'u99', tenantId: 't1', roles: [] };
275
- const filter = compiler.compileFilter([p1, p2], ctx);
276
- expect(filter).toEqual({ $or: [{ owner_id: 'u99' }, { status: 'public' }] });
277
- });
278
-
279
- it('should return null for unsupported expression', () => {
280
- const compiler = new RLSCompiler();
281
- const policy: any = { object: 'x', operation: 'select', using: 'complex expression WITH unsupported syntax' };
282
- const filter = compiler.compileFilter([policy]);
283
- expect(filter).toBeNull();
284
- });
285
-
286
- it('should get applicable policies for object and operation', () => {
287
- const compiler = new RLSCompiler();
288
- const policies: any[] = [
289
- { object: 'task', operation: 'select', using: 'owner_id = current_user.id' },
290
- { object: 'task', operation: 'insert', using: "status = 'open'" },
291
- { object: 'contact', operation: 'all', using: 'tenant_id = current_user.tenant_id' },
292
- { object: '*', operation: 'all', using: "active = 'true'" },
293
- ];
294
-
295
- const taskSelect = compiler.getApplicablePolicies('task', 'find', policies);
296
- expect(taskSelect).toHaveLength(2); // task select + * all
297
- const taskInsert = compiler.getApplicablePolicies('task', 'insert', policies);
298
- expect(taskInsert).toHaveLength(2); // task insert + * all
299
- const contactFind = compiler.getApplicablePolicies('contact', 'find', policies);
300
- expect(contactFind).toHaveLength(2); // contact all + * all
301
- });
302
- });
@@ -1,181 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { Plugin, PluginContext } from '@objectstack/core';
4
- import type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';
5
- import { PermissionEvaluator } from './permission-evaluator.js';
6
- import { RLSCompiler } from './rls-compiler.js';
7
- import { FieldMasker } from './field-masker.js';
8
- import { SysRole, SysPermissionSet } from './objects/index.js';
9
-
10
- /**
11
- * SecurityPlugin
12
- *
13
- * Provides RBAC, Row-Level Security, and Field-Level Security runtime.
14
- * Registers as an engine middleware on the ObjectQL engine.
15
- *
16
- * This plugin is fully optional — without it, the system operates
17
- * without permission checks (same as current behavior).
18
- *
19
- * Dependencies:
20
- * - objectql service (ObjectQL engine with middleware support)
21
- * - metadata service (MetadataFacade for reading permission sets and RLS policies)
22
- */
23
- export class SecurityPlugin implements Plugin {
24
- name = 'com.objectstack.security';
25
- type = 'standard';
26
- version = '1.0.0';
27
- dependencies = ['com.objectstack.engine.objectql'];
28
-
29
- private permissionEvaluator = new PermissionEvaluator();
30
- private rlsCompiler = new RLSCompiler();
31
- private fieldMasker = new FieldMasker();
32
-
33
- async init(ctx: PluginContext): Promise<void> {
34
- ctx.logger.info('Initializing Security Plugin...');
35
-
36
- // Register security services
37
- ctx.registerService('security.permissions', this.permissionEvaluator);
38
- ctx.registerService('security.rls', this.rlsCompiler);
39
- ctx.registerService('security.fieldMasker', this.fieldMasker);
40
-
41
- // Register security system objects via the manifest service.
42
- ctx.getService<{ register(m: any): void }>('manifest').register({
43
- id: 'com.objectstack.security',
44
- name: 'Security',
45
- version: '1.0.0',
46
- type: 'plugin',
47
- namespace: 'sys',
48
- objects: [SysRole, SysPermissionSet],
49
- });
50
-
51
- // Contribute navigation items to the Setup App (if SetupPlugin is loaded).
52
- try {
53
- const setupNav = ctx.getService<{ contribute(c: any): void }>('setupNav');
54
- if (setupNav) {
55
- setupNav.contribute({
56
- areaId: 'area_administration',
57
- items: [
58
- { id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'role', icon: 'shield-check', order: 60 },
59
- { id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'permission_set', icon: 'lock', order: 70 },
60
- ],
61
- });
62
- ctx.logger.info('Security navigation items contributed to Setup App');
63
- }
64
- } catch {
65
- // SetupPlugin not loaded — skip silently
66
- }
67
-
68
- ctx.logger.info('Security Plugin initialized');
69
- }
70
-
71
- async start(ctx: PluginContext): Promise<void> {
72
- ctx.logger.info('Starting Security Plugin...');
73
-
74
- // Get required services
75
- let ql: any;
76
- let metadata: any;
77
-
78
- try {
79
- ql = ctx.getService('objectql');
80
- metadata = ctx.getService('metadata');
81
- } catch (e) {
82
- ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');
83
- return;
84
- }
85
-
86
- if (!ql || typeof ql.registerMiddleware !== 'function') {
87
- ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');
88
- return;
89
- }
90
-
91
- // Register security middleware
92
- ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {
93
- // System operations bypass security
94
- if (opCtx.context?.isSystem) {
95
- return next();
96
- }
97
-
98
- const roles = opCtx.context?.roles ?? [];
99
-
100
- // Skip security checks if no roles (anonymous/unauthenticated)
101
- // The auth middleware should handle authentication separately
102
- if (roles.length === 0 && !opCtx.context?.userId) {
103
- return next();
104
- }
105
-
106
- // 1. Resolve permission sets for the user's roles
107
- let permissionSets: PermissionSet[] = [];
108
- try {
109
- permissionSets = this.permissionEvaluator.resolvePermissionSets(roles, metadata);
110
- } catch (e) {
111
- // If metadata service is misconfigured, log and continue without permission checks
112
- // rather than blocking all operations
113
- return next();
114
- }
115
-
116
- // 2. CRUD permission check
117
- if (permissionSets.length > 0) {
118
- const allowed = this.permissionEvaluator.checkObjectPermission(
119
- opCtx.operation,
120
- opCtx.object,
121
- permissionSets
122
- );
123
-
124
- if (!allowed) {
125
- throw new Error(
126
- `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +
127
- `is not permitted for roles [${roles.join(', ')}]`
128
- );
129
- }
130
- }
131
-
132
- // 3. RLS filter injection
133
- const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);
134
- if (allRlsPolicies.length > 0 && opCtx.ast) {
135
- const rlsFilter = this.rlsCompiler.compileFilter(allRlsPolicies, opCtx.context);
136
- if (rlsFilter) {
137
- if (opCtx.ast.where) {
138
- opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };
139
- } else {
140
- opCtx.ast.where = rlsFilter;
141
- }
142
- }
143
- }
144
-
145
- await next();
146
-
147
- // 4. Field-level security: mask restricted fields in read results
148
- if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {
149
- const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);
150
- if (Object.keys(fieldPerms).length > 0) {
151
- opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);
152
- }
153
- }
154
- });
155
-
156
- ctx.logger.info('Security middleware registered on ObjectQL engine');
157
- }
158
-
159
- async destroy(): Promise<void> {
160
- // No cleanup needed
161
- }
162
-
163
- /**
164
- * Collect all RLS policies from permission sets applicable to the given object/operation.
165
- */
166
- private collectRLSPolicies(
167
- permissionSets: PermissionSet[],
168
- objectName: string,
169
- operation: string
170
- ): RowLevelSecurityPolicy[] {
171
- const allPolicies: RowLevelSecurityPolicy[] = [];
172
-
173
- for (const ps of permissionSets) {
174
- if (ps.rowLevelSecurity) {
175
- allPolicies.push(...ps.rowLevelSecurity);
176
- }
177
- }
178
-
179
- return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);
180
- }
181
- }
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "types": [
7
- "node"
8
- ]
9
- },
10
- "include": [
11
- "src/**/*"
12
- ],
13
- "exclude": [
14
- "dist",
15
- "node_modules",
16
- "**/*.test.ts"
17
- ]
18
- }