-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathbootstrap-declared-sharing-rules.ts
More file actions
148 lines (139 loc) · 6.85 KB
/
Copy pathbootstrap-declared-sharing-rules.ts
File metadata and controls
148 lines (139 loc) · 6.85 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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
/**
* bootstrapDeclaredSharingRules — seed stack-declared `sharingRules` into
* `sys_sharing_rule` (ADR-0057 D6, closes #2077; reconciles #1887).
*
* The spec authoring shape (`SharingRuleSchema`: CEL `condition`,
* `sharedWith{type,value}`) diverges from the enforced runtime shape
* (`criteria_json` JSON filter + `recipient_type`/`recipient_id`). ADR-0057 D6
* makes the RUNTIME shape canonical and translates the authorable fields.
* Every currently-authorable recipient (`user` / `team` / `position` /
* `unit_and_subordinates` / `business_unit`) maps 1:1 and ENFORCES — the
* retired `group`/`guest` recipients and `owner`-type rules no longer parse
* at the spec (ADR-0078; `group` was renamed → `team`). What the runtime
* still cannot enforce is SKIPPED (logged) rather than seeded as a match-all
* rule — silently over-sharing would be worse than not enforcing (ADR-0049):
* - a CEL `condition` the canonical compiler cannot lower (functions,
* cross-object traversal) — ADR-0058 D2. Compound predicates (AND/OR,
* comparisons, null, in) DO lower and are enforced (ADR-0058 D3, #1887).
* - defensively, any stale pre-built package that still registers an old
* `owner`-type / unmapped-recipient shape.
*
* Seeding upserts via `SharingRuleService.defineRule` (idempotent by name) and
* MUST run before `listRules()`/`bindRuleHooks` so the lifecycle hooks bind to
* a populated table.
*/
import type { SharingRuleService } from './sharing-rule-service.js';
import type { SharingRuleRecipientType, ShareAccessLevel } from '@objectstack/spec/contracts';
import { compileCelToFilter } from '@objectstack/formula';
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;
type Logger = { info?: (m: string, meta?: any) => void; warn?: (m: string, meta?: any) => void };
/** Map the spec `sharedWith.type` onto a runtime recipient_type, or null. */
function mapRecipientType(t: unknown): SharingRuleRecipientType | null {
switch (t) {
case 'user': return 'user';
// Flat sys_team membership (ADR-0090 D3 vocabulary; the pre-D3 `group`
// spelling was retired) — expanded by TeamGraphService in expandRecipient.
case 'team': return 'team';
case 'position': return 'position';
// ADR-0057 D5: business-unit subtree recipient.
case 'business_unit': return 'business_unit' as SharingRuleRecipientType;
case 'unit_and_subordinates': return 'unit_and_subordinates' as SharingRuleRecipientType;
// Defensive only: the authoring enum matches the cases above 1:1, but a
// stale pre-built package could still register a retired shape — skip,
// never seed match-all.
default: return null;
}
}
/**
* Compile a sharing-rule CEL `condition` into the runtime `criteria_json`
* FilterCondition (ADR-0058 D1, the substance of #1887).
*
* Delegates to the ONE canonical CEL → FilterCondition pushdown compiler in
* `@objectstack/formula`. A sharing condition is a pure record predicate — no
* `current_user.*` — so it resolves with the default `record` field root and an
* empty variable scope. This lowers the full pushdown subset (`==`/`!=`,
* comparisons, `in`, `&&`/`||`/`!`, `== null`, string ops), not just the former
* `record.field == <literal>` shape, so compound criteria now SEED and ENFORCE
* instead of being skipped as experimental. Anything non-pushdownable (functions,
* cross-object traversal) still returns null → the caller skips it (logged),
* never seeding a permissive match-all (ADR-0049).
*/
export function celToFilter(cel: unknown): Record<string, unknown> | null {
const result = compileCelToFilter(cel as string | { source?: string }, { variables: {} });
return result.ok ? (result.filter as Record<string, unknown>) : null;
}
function readDeclared(engine: any, type: string): any[] {
try {
const reg = engine?._registry;
if (reg?.listItems) {
return (reg.listItems(type) ?? []).map((i: any) => i?.content ?? i).filter(Boolean);
}
} catch { /* fall through */ }
return [];
}
export async function bootstrapDeclaredSharingRules(
ruleService: SharingRuleService,
metadataService: any,
engine: any,
logger?: Logger,
): Promise<{ seeded: number; skipped: number }> {
let rules: any[] = readDeclared(engine, 'sharing_rule');
if (rules.length === 0) {
try {
const listed = metadataService?.list?.('sharing_rule');
rules = typeof (listed as any)?.then === 'function' ? await listed : (listed ?? []);
} catch { rules = []; }
}
if (!Array.isArray(rules) || rules.length === 0) return { seeded: 0, skipped: 0 };
let seeded = 0;
let skipped = 0;
for (const r of rules) {
if (!r?.name || !r?.object) { skipped += 1; continue; }
const recipientType = mapRecipientType(r.sharedWith?.type);
if (!recipientType || !r.sharedWith?.value) {
logger?.warn?.('[sharing-rule] skipped (unmappable recipient) [experimental]', { rule: r.name, sharedWith: r.sharedWith?.type });
skipped += 1; continue;
}
// Defensive: `owner`-type rules were removed from the authoring spec
// (live-membership-dependent, no static criteria_json equivalent) — this
// guards stale pre-built packages that still register the old shape.
if (r.type === 'owner') {
logger?.warn?.('[sharing-rule] skipped owner-based rule (retired authoring shape — use a criteria rule)', { rule: r.name });
skipped += 1; continue;
}
// criteria rules: translate CEL → filter. Empty condition = match-all (intentional).
let criteria: Record<string, unknown> | undefined;
if (r.condition != null && String(r.condition).trim() !== '') {
const f = celToFilter(r.condition);
if (!f) {
logger?.warn?.('[sharing-rule] skipped (untranslatable CEL condition) [experimental]', { rule: r.name, condition: r.condition });
skipped += 1; continue;
}
criteria = f;
}
try {
await ruleService.defineRule({
name: r.name,
label: r.label ?? r.name,
description: r.description ?? undefined,
object: r.object,
criteria,
recipientType,
recipientId: String(r.sharedWith.value),
accessLevel: (r.accessLevel ?? 'read') as ShareAccessLevel,
active: r.active !== false,
// [#2909 P0] Declared rules ship with the app/package → seed mode:
// pristine rows keep receiving declared updates; admin-authored or
// customized rows are never clobbered (defineRule seed-not-clobber).
managedBy: 'package',
} as any, SYSTEM_CTX as any);
seeded += 1;
} catch (err: any) {
logger?.warn?.('[sharing-rule] seed failed', { rule: r.name, error: err?.message });
skipped += 1;
}
}
logger?.info?.('[sharing-rule] declared rules seeded into sys_sharing_rule', { seeded, skipped, total: rules.length });
return { seeded, skipped };
}