|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * MEASUREMENT RIG (temporary shape) — what `granted_by` stores per writer case, |
| 5 | + * over a real `ObjectQL` engine and a real SQL driver with the real |
| 6 | + * `SecurityPlugin` middleware (and so the real `DelegatedAdminGate`) in front. |
| 7 | + */ |
| 8 | + |
| 9 | +import { describe, it, expect, vi, afterEach } from 'vitest'; |
| 10 | +import { ObjectQL } from '@objectstack/objectql'; |
| 11 | +import { SqlDriver } from '@objectstack/driver-sql'; |
| 12 | +import { PermissionSetSchema } from '@objectstack/spec/security'; |
| 13 | + |
| 14 | +import { SecurityPlugin } from './security-plugin.js'; |
| 15 | +import { createInvitationPlacementService } from './invitation-placement.js'; |
| 16 | +import { reconcileOrgAdminGrant } from './auto-org-admin-grant.js'; |
| 17 | +import { |
| 18 | + SysPermissionSet, |
| 19 | + SysPosition, |
| 20 | + SysPositionPermissionSet, |
| 21 | + SysUserPermissionSet, |
| 22 | + SysUserPosition, |
| 23 | +} from './objects/index.js'; |
| 24 | + |
| 25 | +const SYS = { context: { isSystem: true } } as never; |
| 26 | + |
| 27 | +const pk = { name: 'id', type: 'text' as const, primaryKey: true }; |
| 28 | +const text = (name: string) => ({ name, type: 'text' as const }); |
| 29 | +const inline = (name: string, fields: string[]) => ({ |
| 30 | + name, |
| 31 | + label: name, |
| 32 | + fields: { id: pk, ...Object.fromEntries(fields.map((f) => [f, text(f)])) }, |
| 33 | +}); |
| 34 | + |
| 35 | +const TENANT_ADMIN = PermissionSetSchema.parse({ |
| 36 | + name: 'qa_tenant_admin', |
| 37 | + objects: { |
| 38 | + '*': { |
| 39 | + allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true, |
| 40 | + viewAllRecords: true, modifyAllRecords: true, |
| 41 | + }, |
| 42 | + }, |
| 43 | +}); |
| 44 | + |
| 45 | +const CRUD = { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true, viewAllRecords: true }; |
| 46 | +const DELEGATE = PermissionSetSchema.parse({ |
| 47 | + name: 'qa_delegate', |
| 48 | + objects: { |
| 49 | + sys_user_permission_set: CRUD, |
| 50 | + sys_user_position: CRUD, |
| 51 | + sys_permission_set: { allowRead: true, viewAllRecords: true }, |
| 52 | + sys_position: { allowRead: true, viewAllRecords: true }, |
| 53 | + sys_user: { allowRead: true, viewAllRecords: true }, |
| 54 | + sys_business_unit: { allowRead: true, viewAllRecords: true }, |
| 55 | + }, |
| 56 | + adminScope: { |
| 57 | + businessUnit: 'east', |
| 58 | + includeSubtree: true, |
| 59 | + manageAssignments: true, |
| 60 | + manageBindings: false, |
| 61 | + authorEnvironmentSets: false, |
| 62 | + assignablePermissionSets: ['qa_plain'], |
| 63 | + }, |
| 64 | +}); |
| 65 | + |
| 66 | +const ADMIN = 'usr_admin'; |
| 67 | +const DELEGATE_USER = 'usr_delegate'; |
| 68 | +const TARGET = 'usr_target'; |
| 69 | +const FORGED = 'usr_forged'; |
| 70 | +const ISSUER = 'usr_issuer'; |
| 71 | + |
| 72 | +const engines: ObjectQL[] = []; |
| 73 | +afterEach(async () => { |
| 74 | + while (engines.length) { |
| 75 | + try { await engines.pop()?.destroy(); } catch { /* noop */ } |
| 76 | + } |
| 77 | +}); |
| 78 | + |
| 79 | +async function boot() { |
| 80 | + const engine = new ObjectQL(); |
| 81 | + engine.registerDriver( |
| 82 | + new SqlDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true }) as never, |
| 83 | + true, |
| 84 | + ); |
| 85 | + await engine.init(); |
| 86 | + engine.registerApp({ |
| 87 | + id: 'com.objectstack.qa.granted-by-writer-provenance', |
| 88 | + name: 'granted_by writer provenance', |
| 89 | + version: '1.0.0', |
| 90 | + type: 'plugin', |
| 91 | + scope: 'system', |
| 92 | + objects: [ |
| 93 | + inline('sys_user', ['name', 'primary_business_unit_id']), |
| 94 | + inline('sys_organization', ['name']), |
| 95 | + inline('sys_business_unit', ['name', 'parent_business_unit_id', 'organization_id']), |
| 96 | + inline('sys_business_unit_member', ['user_id', 'business_unit_id', 'organization_id']), |
| 97 | + inline('sys_member', ['user_id', 'organization_id', 'role', 'created_at']), |
| 98 | + SysPermissionSet, |
| 99 | + SysPosition, |
| 100 | + SysPositionPermissionSet, |
| 101 | + SysUserPermissionSet, |
| 102 | + SysUserPosition, |
| 103 | + ], |
| 104 | + } as never); |
| 105 | + await engine.syncSchemas(); |
| 106 | + engines.push(engine); |
| 107 | + |
| 108 | + await engine.insert('sys_user', [ADMIN, DELEGATE_USER, TARGET, FORGED, ISSUER].map((id) => ({ id, name: id })), SYS); |
| 109 | + await engine.insert('sys_business_unit', [ |
| 110 | + { id: 'bu_hq', name: 'hq', parent_business_unit_id: null }, |
| 111 | + { id: 'bu_east', name: 'east', parent_business_unit_id: 'bu_hq' }, |
| 112 | + ], SYS); |
| 113 | + await engine.insert('sys_business_unit_member', [ |
| 114 | + { id: 'bum_target', user_id: TARGET, business_unit_id: 'bu_east' }, |
| 115 | + ], SYS); |
| 116 | + await engine.insert('sys_permission_set', [ |
| 117 | + { id: 'ps_plain', name: 'qa_plain', label: 'Plain', active: true }, |
| 118 | + ], SYS); |
| 119 | + await engine.insert('sys_position', [{ id: 'pos_rep', name: 'qa_rep', label: 'Rep', active: true }], SYS); |
| 120 | + await engine.insert('sys_position_permission_set', [ |
| 121 | + { id: 'pps_rep', position_id: 'pos_rep', permission_set_id: 'ps_plain' }, |
| 122 | + ], SYS); |
| 123 | + |
| 124 | + const registered: Record<string, unknown> = {}; |
| 125 | + const services: Record<string, unknown> = { |
| 126 | + manifest: { register: vi.fn() }, |
| 127 | + objectql: engine, |
| 128 | + metadata: { |
| 129 | + get: async (_type: string, name: string) => engine.getSchema(name) ?? null, |
| 130 | + list: async () => [TENANT_ADMIN, DELEGATE], |
| 131 | + }, |
| 132 | + }; |
| 133 | + const ctx = { |
| 134 | + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, |
| 135 | + registerService: vi.fn((name: string, svc: unknown) => { registered[name] = svc; }), |
| 136 | + hook: () => undefined, |
| 137 | + getService: (name: string) => { |
| 138 | + if (!(name in services)) throw new Error(`service not registered: ${name}`); |
| 139 | + return services[name]; |
| 140 | + }, |
| 141 | + }; |
| 142 | + const plugin = new SecurityPlugin({ fallbackPermissionSet: 'qa_tenant_admin' }); |
| 143 | + await plugin.init(ctx as never); |
| 144 | + await plugin.start(ctx as never); |
| 145 | + return { engine, registered, plugin }; |
| 146 | +} |
| 147 | + |
| 148 | +const asAdmin = { userId: ADMIN, positions: [], permissions: ['qa_tenant_admin'] }; |
| 149 | +const asDelegate = { userId: DELEGATE_USER, positions: [], permissions: ['qa_delegate'] }; |
| 150 | + |
| 151 | +const grantRow = (extra: Record<string, unknown> = {}) => ({ |
| 152 | + user_id: TARGET, permission_set_id: 'ps_plain', ...extra, |
| 153 | +}); |
| 154 | +const assignmentRow = (extra: Record<string, unknown> = {}) => ({ |
| 155 | + user_id: TARGET, position: 'qa_rep', business_unit_id: 'bu_east', ...extra, |
| 156 | +}); |
| 157 | + |
| 158 | +async function stored(engine: ObjectQL, object: string, id: unknown) { |
| 159 | + return (await engine.findOne(object, { where: { id }, context: { isSystem: true } } as never)) as any; |
| 160 | +} |
| 161 | + |
| 162 | +async function attempt(p: Promise<any>) { |
| 163 | + return p.then((r) => ({ ok: true as const, r }), (e) => ({ ok: false as const, code: e?.code, msg: String(e?.message) })); |
| 164 | +} |
| 165 | + |
| 166 | +describe('MEASURE granted_by per writer case', () => { |
| 167 | + for (const [object, row] of [ |
| 168 | + ['sys_user_permission_set', grantRow], |
| 169 | + ['sys_user_position', assignmentRow], |
| 170 | + ] as const) { |
| 171 | + for (const [who, ctx] of [['tenant-admin', asAdmin], ['delegate', asDelegate]] as const) { |
| 172 | + for (const explicit of [true, false]) { |
| 173 | + it(`${object} · ${who} · ${explicit ? 'explicit granted_by' : 'no granted_by'}`, async () => { |
| 174 | + const { engine } = await boot(); |
| 175 | + const out = await attempt(engine.insert(object, row(explicit ? { granted_by: FORGED } : {}), { context: { ...ctx } } as never)); |
| 176 | + const s = out.ok ? await stored(engine, object, out.r?.id) : null; |
| 177 | + console.log(`MEASURE ${object} ${who} explicit=${explicit} ->`, JSON.stringify(out.ok ? { granted_by: s?.granted_by ?? null } : out)); |
| 178 | + }); |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + it(`${object} · system insert with a value`, async () => { |
| 183 | + const { engine } = await boot(); |
| 184 | + const out = await attempt(engine.insert(object, row({ granted_by: FORGED }), SYS)); |
| 185 | + const s = out.ok ? await stored(engine, object, out.r?.id) : null; |
| 186 | + console.log(`MEASURE ${object} system explicit=true ->`, JSON.stringify(out.ok ? { granted_by: s?.granted_by ?? null } : out)); |
| 187 | + }); |
| 188 | + |
| 189 | + for (const [who, ctx] of [['tenant-admin', asAdmin], ['delegate', asDelegate]] as const) { |
| 190 | + it(`${object} · ${who} · UPDATE granted_by`, async () => { |
| 191 | + const { engine } = await boot(); |
| 192 | + const created = await engine.insert(object, row({ granted_by: ISSUER }), SYS); |
| 193 | + const out = await attempt(engine.update(object, { granted_by: FORGED }, { where: { id: (created as any).id }, context: { ...ctx } } as never)); |
| 194 | + const s = await stored(engine, object, (created as any).id); |
| 195 | + console.log(`MEASURE ${object} ${who} UPDATE ->`, JSON.stringify({ outcome: out.ok ? 'landed' : out, granted_by: s?.granted_by ?? null })); |
| 196 | + }); |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + it('invitation placement (system) apply', async () => { |
| 201 | + const { engine, registered } = await boot(); |
| 202 | + const svc: any = Object.values(registered).find((s: any) => s && typeof s.apply === 'function'); |
| 203 | + const res = await svc.apply({ intent: { businessUnitId: 'bu_east', positions: ['qa_rep'] }, userId: TARGET, grantedBy: ISSUER }); |
| 204 | + const rows = await engine.find('sys_user_position', { where: { user_id: TARGET }, context: { isSystem: true } } as never) as any[]; |
| 205 | + console.log('MEASURE invitation-placement ->', JSON.stringify({ res, granted_by: rows.map((r) => r.granted_by) })); |
| 206 | + }); |
| 207 | + |
| 208 | + it('auto-org-admin reconcile (system) with attributed human', async () => { |
| 209 | + const { engine } = await boot(); |
| 210 | + await engine.insert('sys_permission_set', [ |
| 211 | + { id: 'ps_oa', name: 'organization_admin', label: 'OA', active: true }, |
| 212 | + { id: 'ps_oa_nb', name: 'organization_admin_no_bypass', label: 'OA nb', active: true }, |
| 213 | + ], SYS); |
| 214 | + await engine.insert('sys_member', { id: 'm1', user_id: TARGET, organization_id: 'o1', role: 'owner', created_at: new Date().toISOString() }, SYS); |
| 215 | + const res = await reconcileOrgAdminGrant(engine, TARGET, 'o1', { posture: 'single', attributedUserId: ISSUER }); |
| 216 | + const rows = await engine.find('sys_user_permission_set', { where: { user_id: TARGET }, context: { isSystem: true } } as never) as any[]; |
| 217 | + console.log('MEASURE auto-org-admin ->', JSON.stringify({ res, granted_by: rows.map((r) => r.granted_by) })); |
| 218 | + }); |
| 219 | +}); |
| 220 | + |
| 221 | +void createInvitationPlacementService; |
| 222 | +void expect; |
0 commit comments