-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathmanaged-object-write-denies.test.ts
More file actions
108 lines (95 loc) · 4.88 KB
/
Copy pathmanaged-object-write-denies.test.ts
File metadata and controls
108 lines (95 loc) · 4.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// ADR-0092 / ADR-0103 / #3325 — registry-driven managed-object write denies.
import { describe, it, expect } from 'vitest';
import {
applyManagedWriteDenies,
MANAGED_DENY_ENTRY,
MANAGED_DENY_TARGET_SETS,
} from './managed-object-write-denies.js';
import { MCP_AGENT_PERMISSION_SET_WRITE, MCP_AGENT_PERMISSION_SET_READ } from '@objectstack/spec/ai';
// Minimal PermissionSet-shaped fixtures (only name + objects matter here).
const set = (name: string, objects: Record<string, unknown> = {}): any => ({ name, objects });
const DENY = { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false };
const schemas = [
{ name: 'sys_user', managedBy: 'better-auth', userActions: { edit: true } }, // intentional divergence
{ name: 'sys_sso_provider', managedBy: 'better-auth' },
{ name: 'crm_lead', managedBy: 'platform' },
{ name: 'sys_setting', managedBy: 'system' },
{ name: 'sys_audit_log', managedBy: 'append-only' },
{ name: 'sys_sharing_rule', managedBy: 'config' },
{ name: 'sys_no_bucket' }, // unset
];
describe('applyManagedWriteDenies (#3325)', () => {
it('injects a read-only-write deny for every better-auth object into the four target sets', () => {
const sets = [
set('organization_admin'),
set('member_default'),
set('viewer_readonly'),
set(MCP_AGENT_PERMISSION_SET_WRITE),
];
const res = applyManagedWriteDenies(sets, schemas);
// 2 better-auth objects × 4 sets = 8 injections.
expect(res.applied).toBe(8);
expect(res.skippedExisting).toBe(0);
for (const s of sets) {
expect(s.objects.sys_user).toEqual(DENY);
expect(s.objects.sys_sso_provider).toEqual(DENY);
}
});
it('hard-denies sys_user despite userActions.edit:true (permission-set booleans cannot whitelist fields)', () => {
const s = set('member_default');
applyManagedWriteDenies([s], schemas);
expect(s.objects.sys_user).toEqual(DENY);
expect(s.objects.sys_user.allowEdit).toBe(false);
});
it('ignores platform / config / system / append-only / unset buckets (pins the ADR-0103 deferral)', () => {
const s = set('member_default');
applyManagedWriteDenies([s], schemas);
for (const name of ['crm_lead', 'sys_setting', 'sys_audit_log', 'sys_sharing_rule', 'sys_no_bucket']) {
expect(s.objects[name]).toBeUndefined();
}
});
it('never touches admin_full_access or the MCP read/restricted sets', () => {
const admin = set('admin_full_access', { '*': { allowRead: true, allowCreate: true } });
const mcpRead = set(MCP_AGENT_PERMISSION_SET_READ, { '*': { allowRead: true } });
const res = applyManagedWriteDenies([admin, mcpRead], schemas);
expect(res.applied).toBe(0);
expect(admin.objects).toEqual({ '*': { allowRead: true, allowCreate: true } });
expect(mcpRead.objects.sys_user).toBeUndefined();
});
it('never overrides an existing explicit entry (protects the org-admin RBAC block / static baseline)', () => {
// Mirror org-admin: sys_user already carved out read-only; keep it verbatim.
const existing = { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false, viewAllRecords: true };
const s = set('organization_admin', { sys_user: existing });
const res = applyManagedWriteDenies([s], schemas);
expect(s.objects.sys_user).toBe(existing); // same reference, untouched (viewAllRecords preserved)
expect(res.skippedExisting).toBe(1); // sys_user skipped
expect(res.applied).toBe(1); // sys_sso_provider injected
expect(s.objects.sys_sso_provider).toEqual(DENY);
});
it('is idempotent — a second apply injects nothing', () => {
const s = set('viewer_readonly');
const first = applyManagedWriteDenies([s], schemas);
const second = applyManagedWriteDenies([s], schemas);
expect(first.applied).toBe(2);
expect(second.applied).toBe(0);
expect(second.skippedExisting).toBe(2);
});
it('injects a distinct object per entry (no shared reference across tables)', () => {
const s = set('member_default');
applyManagedWriteDenies([s], schemas);
expect(s.objects.sys_user).not.toBe(s.objects.sys_sso_provider);
expect(s.objects.sys_user).not.toBe(MANAGED_DENY_ENTRY);
});
it('tolerates empty / malformed input', () => {
expect(applyManagedWriteDenies([], schemas)).toEqual({ applied: 0, skippedExisting: 0 });
expect(applyManagedWriteDenies([set('member_default')], [])).toEqual({ applied: 0, skippedExisting: 0 });
// a target set with no objects map is skipped, not thrown on
expect(() => applyManagedWriteDenies([{ name: 'member_default' } as any], schemas)).not.toThrow();
});
it('the target allowlist is exactly the four write-granting sets', () => {
expect([...MANAGED_DENY_TARGET_SETS].sort()).toEqual(
['member_default', 'organization_admin', 'viewer_readonly', MCP_AGENT_PERMISSION_SET_WRITE].sort(),
);
});
});