@bevel-software/platform-core-backend 0.19.0 → 0.20.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.
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +22 -0
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +3 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +10 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +54 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -0
- package/dist/modules/audit/agent-audit.service.js +290 -0
- package/dist/modules/audit/agent-audit.service.js.map +1 -0
- package/dist/modules/audit/agent-rest-audit.middleware.d.ts +40 -0
- package/dist/modules/audit/agent-rest-audit.middleware.d.ts.map +1 -0
- package/dist/modules/audit/agent-rest-audit.middleware.js +75 -0
- package/dist/modules/audit/agent-rest-audit.middleware.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +145 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -0
- package/dist/modules/audit/audit.contract.js +35 -0
- package/dist/modules/audit/audit.contract.js.map +1 -0
- package/dist/modules/audit/audit.routes.d.ts +21 -0
- package/dist/modules/audit/audit.routes.d.ts.map +1 -0
- package/dist/modules/audit/audit.routes.js +157 -0
- package/dist/modules/audit/audit.routes.js.map +1 -0
- package/dist/modules/audit/event-classifier.d.ts +52 -0
- package/dist/modules/audit/event-classifier.d.ts.map +1 -0
- package/dist/modules/audit/event-classifier.js +123 -0
- package/dist/modules/audit/event-classifier.js.map +1 -0
- package/dist/modules/audit/index.d.ts +7 -0
- package/dist/modules/audit/index.d.ts.map +1 -0
- package/dist/modules/audit/index.js +7 -0
- package/dist/modules/audit/index.js.map +1 -0
- package/dist/modules/audit/request-audit.d.ts +57 -0
- package/dist/modules/audit/request-audit.d.ts.map +1 -0
- package/dist/modules/audit/request-audit.js +117 -0
- package/dist/modules/audit/request-audit.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +414 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +99 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +6 -1
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +9 -1
- package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/git-guarded-filesystem.js +16 -3
- package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +1 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +32 -0
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +8 -1
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +9 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +48 -5
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +59 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +180 -8
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +10 -2
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +32 -6
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +21 -5
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +22 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +10 -0
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +1 -0
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +8 -0
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +8 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +9 -0
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +49 -13
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/git-internals.d.ts +7 -1
- package/dist/shared/git-internals.d.ts.map +1 -1
- package/dist/shared/git-internals.js +67 -9
- package/dist/shared/git-internals.js.map +1 -1
- package/migrations/0012_agent_audit.sql +41 -0
- package/migrations/0013_agent_identity_and_key_soft_delete.sql +29 -0
- package/migrations/meta/0012_snapshot.json +2247 -0
- package/migrations/meta/0013_snapshot.json +2259 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +4 -4
- package/src/core/create-core-server.ts +28 -0
- package/src/core/create-core-services.ts +13 -0
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +370 -0
- package/src/modules/audit/__tests__/agent-rest-audit.middleware.test.ts +113 -0
- package/src/modules/audit/__tests__/audit.routes.test.ts +201 -0
- package/src/modules/audit/__tests__/event-classifier.test.ts +121 -0
- package/src/modules/audit/agent-audit.service.ts +316 -0
- package/src/modules/audit/agent-rest-audit.middleware.ts +83 -0
- package/src/modules/audit/audit.contract.ts +164 -0
- package/src/modules/audit/audit.routes.ts +168 -0
- package/src/modules/audit/event-classifier.ts +155 -0
- package/src/modules/audit/index.ts +14 -0
- package/src/modules/audit/request-audit.ts +141 -0
- package/src/modules/database/core-schema.ts +104 -0
- package/src/modules/diff/diff.service.ts +6 -1
- package/src/modules/kb-fs/git-guarded-filesystem.ts +16 -3
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +183 -13
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -2
- package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +24 -4
- package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +1 -1
- package/src/modules/mcp/__tests__/mcp.service.test.ts +120 -3
- package/src/modules/mcp/mcp-auth.middleware.ts +31 -0
- package/src/modules/mcp/mcp.routes.ts +11 -1
- package/src/modules/mcp/mcp.service.ts +62 -4
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +184 -6
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +35 -0
- package/src/modules/settings/deployment-settings.service.ts +47 -8
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +29 -8
- package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +6 -0
- package/src/modules/tool-auth/external-api-key.interface.ts +22 -5
- package/src/modules/tool-auth/external-api-key.service.ts +25 -8
- package/src/modules/tool-auth/internal-token.service.ts +11 -0
- package/src/modules/tool-auth/tool-auth.middleware.ts +17 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +135 -44
- package/src/modules/workspace/workspace.service.ts +9 -0
- package/src/modules/workspace/workspace.tools.ts +47 -11
- package/src/shared/__tests__/git-internals.test.ts +41 -0
- package/src/shared/git-internals.ts +61 -6
- package/dist/modules/access/access-errors.d.ts +0 -34
- package/dist/modules/access/access-errors.d.ts.map +0 -1
- package/dist/modules/access/access-errors.js +0 -40
- package/dist/modules/access/access-errors.js.map +0 -1
- package/dist/modules/access/access-splice.d.ts +0 -140
- package/dist/modules/access/access-splice.d.ts.map +0 -1
- package/dist/modules/access/access-splice.js +0 -389
- package/dist/modules/access/access-splice.js.map +0 -1
- package/dist/modules/access/group-files.d.ts +0 -83
- package/dist/modules/access/group-files.d.ts.map +0 -1
- package/dist/modules/access/group-files.js +0 -167
- package/dist/modules/access/group-files.js.map +0 -1
- package/dist/modules/access/kb-read-filter.d.ts +0 -41
- package/dist/modules/access/kb-read-filter.d.ts.map +0 -1
- package/dist/modules/access/kb-read-filter.js +0 -60
- package/dist/modules/access/kb-read-filter.js.map +0 -1
- package/dist/modules/access/render-roles-yaml.d.ts +0 -22
- package/dist/modules/access/render-roles-yaml.d.ts.map +0 -1
- package/dist/modules/access/render-roles-yaml.js +0 -56
- package/dist/modules/access/render-roles-yaml.js.map +0 -1
- package/dist/modules/access/roles-yaml-guard.d.ts +0 -56
- package/dist/modules/access/roles-yaml-guard.d.ts.map +0 -1
- package/dist/modules/access/roles-yaml-guard.js +0 -79
- package/dist/modules/access/roles-yaml-guard.js.map +0 -1
- package/dist/modules/diff/diff-paths.d.ts +0 -2
- package/dist/modules/diff/diff-paths.d.ts.map +0 -1
- package/dist/modules/diff/diff-paths.js +0 -9
- package/dist/modules/diff/diff-paths.js.map +0 -1
- package/dist/modules/groups/group-provision.service.d.ts +0 -130
- package/dist/modules/groups/group-provision.service.d.ts.map +0 -1
- package/dist/modules/groups/group-provision.service.js +0 -288
- package/dist/modules/groups/group-provision.service.js.map +0 -1
- package/dist/modules/groups/groups.contract.d.ts +0 -106
- package/dist/modules/groups/groups.contract.d.ts.map +0 -1
- package/dist/modules/groups/groups.contract.js +0 -36
- package/dist/modules/groups/groups.contract.js.map +0 -1
- package/dist/modules/groups/groups.routes.d.ts +0 -42
- package/dist/modules/groups/groups.routes.d.ts.map +0 -1
- package/dist/modules/groups/groups.routes.js +0 -379
- package/dist/modules/groups/groups.routes.js.map +0 -1
- package/dist/modules/groups/groups.service.d.ts +0 -60
- package/dist/modules/groups/groups.service.d.ts.map +0 -1
- package/dist/modules/groups/groups.service.js +0 -172
- package/dist/modules/groups/groups.service.js.map +0 -1
- package/dist/modules/groups/index.d.ts +0 -7
- package/dist/modules/groups/index.d.ts.map +0 -1
- package/dist/modules/groups/index.js +0 -6
- package/dist/modules/groups/index.js.map +0 -1
- package/dist/modules/groups/join-proposals.d.ts +0 -53
- package/dist/modules/groups/join-proposals.d.ts.map +0 -1
- package/dist/modules/groups/join-proposals.js +0 -67
- package/dist/modules/groups/join-proposals.js.map +0 -1
- package/dist/modules/groups/join-requests.service.d.ts +0 -81
- package/dist/modules/groups/join-requests.service.d.ts.map +0 -1
- package/dist/modules/groups/join-requests.service.js +0 -135
- package/dist/modules/groups/join-requests.service.js.map +0 -1
- package/dist/modules/mcp/mcp-session-store.d.ts +0 -74
- package/dist/modules/mcp/mcp-session-store.d.ts.map +0 -1
- package/dist/modules/mcp/mcp-session-store.js +0 -131
- package/dist/modules/mcp/mcp-session-store.js.map +0 -1
- package/dist/modules/workflow/file-change-notifier.d.ts +0 -38
- package/dist/modules/workflow/file-change-notifier.d.ts.map +0 -1
- package/dist/modules/workflow/file-change-notifier.js +0 -22
- package/dist/modules/workflow/file-change-notifier.js.map +0 -1
- package/dist/modules/workflow/git/branch-name.d.ts +0 -10
- package/dist/modules/workflow/git/branch-name.d.ts.map +0 -1
- package/dist/modules/workflow/git/branch-name.js +0 -76
- package/dist/modules/workflow/git/branch-name.js.map +0 -1
- package/dist/modules/workflow/git/clone-config.d.ts +0 -61
- package/dist/modules/workflow/git/clone-config.d.ts.map +0 -1
- package/dist/modules/workflow/git/clone-config.js +0 -69
- package/dist/modules/workflow/git/clone-config.js.map +0 -1
- package/dist/modules/workflow/git/mutex.d.ts +0 -11
- package/dist/modules/workflow/git/mutex.d.ts.map +0 -1
- package/dist/modules/workflow/git/mutex.js +0 -23
- package/dist/modules/workflow/git/mutex.js.map +0 -1
- package/dist/modules/workflow/locking-filesystem.d.ts +0 -137
- package/dist/modules/workflow/locking-filesystem.d.ts.map +0 -1
- package/dist/modules/workflow/locking-filesystem.js +0 -553
- package/dist/modules/workflow/locking-filesystem.js.map +0 -1
- package/dist/modules/workflow/read-only-filesystem.d.ts +0 -24
- package/dist/modules/workflow/read-only-filesystem.d.ts.map +0 -1
- package/dist/modules/workflow/read-only-filesystem.js +0 -39
- package/dist/modules/workflow/read-only-filesystem.js.map +0 -1
- package/dist/modules/workflow/workflow.errors.d.ts +0 -197
- package/dist/modules/workflow/workflow.errors.d.ts.map +0 -1
- package/dist/modules/workflow/workflow.errors.js +0 -298
- package/dist/modules/workflow/workflow.errors.js.map +0 -1
- package/dist/modules/workspace/bevel-ignore.d.ts +0 -30
- package/dist/modules/workspace/bevel-ignore.d.ts.map +0 -1
- package/dist/modules/workspace/bevel-ignore.js +0 -61
- package/dist/modules/workspace/bevel-ignore.js.map +0 -1
- package/dist/modules/workspace/kb-seed.interface.d.ts +0 -36
- package/dist/modules/workspace/kb-seed.interface.d.ts.map +0 -1
- package/dist/modules/workspace/kb-seed.interface.js +0 -2
- package/dist/modules/workspace/kb-seed.interface.js.map +0 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +0 -125
- package/dist/modules/workspace/kb-seed.service.d.ts.map +0 -1
- package/dist/modules/workspace/kb-seed.service.js +0 -535
- package/dist/modules/workspace/kb-seed.service.js.map +0 -1
- package/dist/modules/workspace/plugins-migration.d.ts +0 -50
- package/dist/modules/workspace/plugins-migration.d.ts.map +0 -1
- package/dist/modules/workspace/plugins-migration.js +0 -379
- package/dist/modules/workspace/plugins-migration.js.map +0 -1
- package/dist/shared/fs-errors.d.ts +0 -8
- package/dist/shared/fs-errors.d.ts.map +0 -1
- package/dist/shared/fs-errors.js +0 -11
- package/dist/shared/fs-errors.js.map +0 -1
- package/dist/shared/fs-walk.d.ts +0 -23
- package/dist/shared/fs-walk.d.ts.map +0 -1
- package/dist/shared/fs-walk.js +0 -42
- package/dist/shared/fs-walk.js.map +0 -1
- package/dist/shared/hash-email.d.ts +0 -11
- package/dist/shared/hash-email.d.ts.map +0 -1
- package/dist/shared/hash-email.js +0 -14
- package/dist/shared/hash-email.js.map +0 -1
- package/dist/shared/kb-walk.d.ts +0 -52
- package/dist/shared/kb-walk.d.ts.map +0 -1
- package/dist/shared/kb-walk.js +0 -76
- package/dist/shared/kb-walk.js.map +0 -1
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Surgical text-splice editor for `access.md` frontmatter and node-frontmatter
|
|
3
|
-
* access verbs.
|
|
4
|
-
*
|
|
5
|
-
* WHY a splice and not parse→model→emit: the resolver's parser
|
|
6
|
-
* (`parseYamlSubset` / `extractFrontmatter` / `stripComment`) deliberately
|
|
7
|
-
* throws away comments, blank lines, key order, AND the markdown body below the
|
|
8
|
-
* closing `---`. Re-serialising from the parsed model would silently DELETE the
|
|
9
|
-
* prose bodies the real `access.md` files carry (the repo-root one is a whole
|
|
10
|
-
* README) — and a `parse(emit(x))` round-trip test would PASS anyway because
|
|
11
|
-
* parse ignores the body. So every mutation here edits the RAW text: we locate
|
|
12
|
-
* the verb's block, splice exactly one list line in or out, and leave every
|
|
13
|
-
* other byte — comments, ordering, blank lines, the body — untouched.
|
|
14
|
-
*
|
|
15
|
-
* ---
|
|
16
|
-
* write: (verb key line, indent 0)
|
|
17
|
-
* - Admin (list item, indent 2) <- splice point
|
|
18
|
-
* - Felix <f@x.eu> (list item)
|
|
19
|
-
* # a comment (preserved verbatim)
|
|
20
|
-
* owner: [] (inline-empty list)
|
|
21
|
-
* ---
|
|
22
|
-
* # Body prose (preserved verbatim)
|
|
23
|
-
*
|
|
24
|
-
* The same engine serves node-frontmatter, which additionally accepts a scalar
|
|
25
|
-
* form (`owner: Felix <f@x.eu>`). Adding a second principal to a scalar verb
|
|
26
|
-
* normalises it to the block-list form.
|
|
27
|
-
*/
|
|
28
|
-
import { parseAccessEntry, canonicalEmail, canonicalRoleName, accessMdDeclaresBodyRules, } from './access-control.service.js';
|
|
29
|
-
/** Thrown when the input can't be spliced safely (malformed frontmatter, etc.). */
|
|
30
|
-
export class AccessSpliceError extends Error {
|
|
31
|
-
constructor(message) {
|
|
32
|
-
super(message);
|
|
33
|
-
this.name = 'AccessSpliceError';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Control chars (NUL..US) — newlines, tabs, etc. Never allowed in a principal. */
|
|
37
|
-
// eslint-disable-next-line no-control-regex -- intentional control-char injection guard
|
|
38
|
-
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
39
|
-
/**
|
|
40
|
-
* Render a principal as the canonical access-entry line value the parser reads
|
|
41
|
-
* back (`Name <email>` or a bare role display name). The caller is responsible
|
|
42
|
-
* for having validated the principal first (see `validatePrincipal`).
|
|
43
|
-
*
|
|
44
|
-
* `deny` prefixes the entry with `deny ` so the resolver reads it as a denial
|
|
45
|
-
* (the `deny-here` per-item override) — same line shape, opposite verdict.
|
|
46
|
-
*/
|
|
47
|
-
export function renderEntry(p, deny = false) {
|
|
48
|
-
const body = p.kind === 'user' ? `${p.displayName} <${p.email}>` : p.role;
|
|
49
|
-
return deny ? `deny ${body}` : body;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Validate a principal before it is spliced into a file. Rejects anything that
|
|
53
|
-
* could break frontmatter or smuggle in extra YAML — newlines, control chars,
|
|
54
|
-
* and emails / names that don't match the resolver's own regexes. This is the
|
|
55
|
-
* injection guard: a principal that survives this is safe to render verbatim
|
|
56
|
-
* into a ` - ` list line.
|
|
57
|
-
*
|
|
58
|
-
* Returns the canonical principal (lowercased email / canonical role) on
|
|
59
|
-
* success, or throws `AccessSpliceError`.
|
|
60
|
-
*/
|
|
61
|
-
export function validatePrincipal(p) {
|
|
62
|
-
if (p.kind === 'user') {
|
|
63
|
-
const displayName = p.displayName.trim();
|
|
64
|
-
const email = canonicalEmail(p.email);
|
|
65
|
-
if (!displayName)
|
|
66
|
-
throw new AccessSpliceError('person grant needs a display name');
|
|
67
|
-
// `#` is rejected because `uncomment()` truncates a whitespace-preceded `#`
|
|
68
|
-
// on read-back, so a name containing it would not round-trip through blockEntries.
|
|
69
|
-
if (CONTROL_CHARS.test(displayName) ||
|
|
70
|
-
displayName.includes('<') ||
|
|
71
|
-
displayName.includes('>') ||
|
|
72
|
-
displayName.includes('#')) {
|
|
73
|
-
throw new AccessSpliceError(`invalid display name: ${JSON.stringify(p.displayName)}`);
|
|
74
|
-
}
|
|
75
|
-
// Reuse the resolver's own email shape so a granted entry always parses back.
|
|
76
|
-
if (!/^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/.test(email)) {
|
|
77
|
-
throw new AccessSpliceError(`invalid email: ${JSON.stringify(p.email)}`);
|
|
78
|
-
}
|
|
79
|
-
// The rendered `Name <email>` line must round-trip through parseAccessEntry.
|
|
80
|
-
const round = parseAccessEntry(`${displayName} <${email}>`);
|
|
81
|
-
if (!round.ok || round.entry.kind !== 'user' || round.entry.deny) {
|
|
82
|
-
throw new AccessSpliceError(`person grant does not round-trip: ${JSON.stringify(p)}`);
|
|
83
|
-
}
|
|
84
|
-
return { kind: 'user', email, displayName };
|
|
85
|
-
}
|
|
86
|
-
const role = p.role.trim();
|
|
87
|
-
const canonical = canonicalRoleName(role);
|
|
88
|
-
if (!canonical)
|
|
89
|
-
throw new AccessSpliceError('role grant needs a name');
|
|
90
|
-
if (CONTROL_CHARS.test(role) ||
|
|
91
|
-
role.includes('<') ||
|
|
92
|
-
role.includes('>') ||
|
|
93
|
-
role.includes(':') ||
|
|
94
|
-
role.includes('#')) {
|
|
95
|
-
throw new AccessSpliceError(`invalid role name: ${JSON.stringify(p.role)}`);
|
|
96
|
-
}
|
|
97
|
-
// `deny` is never a grantee — it's the denial prefix. `everyone` IS grantable
|
|
98
|
-
// (the built-in public role); the access route restricts it to the read verb.
|
|
99
|
-
if (canonical === 'deny') {
|
|
100
|
-
throw new AccessSpliceError(`'${role}' is a reserved name and cannot be granted as a role`);
|
|
101
|
-
}
|
|
102
|
-
const round = parseAccessEntry(role);
|
|
103
|
-
if (!round.ok || round.entry.kind !== 'role' || round.entry.deny) {
|
|
104
|
-
throw new AccessSpliceError(`role grant does not round-trip: ${JSON.stringify(p)}`);
|
|
105
|
-
}
|
|
106
|
-
return { kind: 'role', role };
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* True when a parsed entry names the given principal (ignoring deny prefix).
|
|
110
|
-
* `tokenMatch` (role-shaped principals only) selects exact-spelling vs
|
|
111
|
-
* name-level matching — see {@link TokenMatch}.
|
|
112
|
-
*/
|
|
113
|
-
function entryMatches(entry, p, tokenMatch) {
|
|
114
|
-
if (p.kind === 'user')
|
|
115
|
-
return entry.kind === 'user' && entry.email === canonicalEmail(p.email);
|
|
116
|
-
if (entry.kind !== 'role')
|
|
117
|
-
return false;
|
|
118
|
-
const canonicalPrincipal = canonicalRoleName(p.role);
|
|
119
|
-
if (tokenMatch === 'exact')
|
|
120
|
-
return entry.role === canonicalPrincipal;
|
|
121
|
-
const strip = (token) => token.startsWith('role/') ? canonicalRoleName(token.slice('role/'.length)) : token;
|
|
122
|
-
return strip(entry.role) === strip(canonicalPrincipal);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Split a file into [pre, frontmatter-lines, post] without losing a byte.
|
|
126
|
-
* `hasFrontmatter` is false when the file has no `---` fence yet — callers that
|
|
127
|
-
* grant into a fresh file synthesise one.
|
|
128
|
-
*/
|
|
129
|
-
function splitFrontmatter(text) {
|
|
130
|
-
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
131
|
-
const lines = text.split(/\r?\n/);
|
|
132
|
-
// Opening fence must be the very first line.
|
|
133
|
-
if (lines[0]?.trim() !== '---') {
|
|
134
|
-
return { pre: [], fm: [], post: lines, eol, hasFrontmatter: false };
|
|
135
|
-
}
|
|
136
|
-
for (let i = 1; i < lines.length; i++) {
|
|
137
|
-
if (lines[i].trim() === '---') {
|
|
138
|
-
return {
|
|
139
|
-
pre: lines.slice(0, 1), // the opening ---
|
|
140
|
-
fm: lines.slice(1, i),
|
|
141
|
-
post: lines.slice(i), // closing --- onward (body preserved)
|
|
142
|
-
eol,
|
|
143
|
-
hasFrontmatter: true,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
throw new AccessSpliceError('unterminated frontmatter — no closing `---`');
|
|
148
|
-
}
|
|
149
|
-
function joinFrontmatter(f) {
|
|
150
|
-
return [...f.pre, ...f.fm, ...f.post].join(f.eol);
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* The line region a mutation edits, per {@link SpliceTarget}. `lines` aliases
|
|
154
|
-
* the region's array (edited in place, like `f.fm` always was); `join()`
|
|
155
|
-
* reassembles the whole file around it.
|
|
156
|
-
*/
|
|
157
|
-
function pickRegion(text, f, target) {
|
|
158
|
-
if (target === 'folder' && f.hasFrontmatter && accessMdDeclaresBodyRules(text)) {
|
|
159
|
-
// New format: post = [closing '---', ...body]. Edit the body lines.
|
|
160
|
-
const closing = f.post[0];
|
|
161
|
-
const body = f.post.slice(1);
|
|
162
|
-
return {
|
|
163
|
-
lines: body,
|
|
164
|
-
join: () => [...f.pre, ...f.fm, closing, ...body].join(f.eol),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
return { lines: f.fm, join: () => joinFrontmatter(f) };
|
|
168
|
-
}
|
|
169
|
-
/** Leading-space count of a line. */
|
|
170
|
-
function indentOf(line) {
|
|
171
|
-
const m = line.match(/^( *)/);
|
|
172
|
-
return m ? m[1].length : 0;
|
|
173
|
-
}
|
|
174
|
-
/** Strip a trailing `# comment` the way the resolver's tokeniser does. */
|
|
175
|
-
function uncomment(line) {
|
|
176
|
-
let inWs = true;
|
|
177
|
-
for (let i = 0; i < line.length; i++) {
|
|
178
|
-
const ch = line[i];
|
|
179
|
-
if (ch === '#' && (inWs || (i > 0 && /\s/.test(line[i - 1]))))
|
|
180
|
-
return line.slice(0, i);
|
|
181
|
-
if (!/\s/.test(ch))
|
|
182
|
-
inWs = false;
|
|
183
|
-
}
|
|
184
|
-
return line;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Locate a verb's block within the frontmatter lines. Walks the raw lines (not
|
|
188
|
-
* the parsed model) so we can address them for splicing. Recognises the three
|
|
189
|
-
* shapes the resolver accepts: a block list, an inline `verb: []`, and (node
|
|
190
|
-
* frontmatter only) an inline scalar `verb: value`.
|
|
191
|
-
*/
|
|
192
|
-
function findVerbBlock(fm, verb) {
|
|
193
|
-
const block = {
|
|
194
|
-
keyLine: -1,
|
|
195
|
-
keyIndent: 0,
|
|
196
|
-
itemLines: [],
|
|
197
|
-
itemIndent: -1,
|
|
198
|
-
inlineEmpty: false,
|
|
199
|
-
inlineScalarValue: null,
|
|
200
|
-
};
|
|
201
|
-
for (let i = 0; i < fm.length; i++) {
|
|
202
|
-
const content = uncomment(fm[i]);
|
|
203
|
-
if (!content.trim())
|
|
204
|
-
continue;
|
|
205
|
-
const indent = indentOf(content);
|
|
206
|
-
const body = content.slice(indent);
|
|
207
|
-
// A list item belonging to the current verb block.
|
|
208
|
-
if ((body.startsWith('- ') || body === '-') &&
|
|
209
|
-
block.keyLine !== -1 &&
|
|
210
|
-
indent > block.keyIndent) {
|
|
211
|
-
block.itemLines.push(i);
|
|
212
|
-
if (block.itemIndent === -1)
|
|
213
|
-
block.itemIndent = indent;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const colon = body.indexOf(':');
|
|
217
|
-
if (colon < 0)
|
|
218
|
-
continue; // not a key line; ignore (the parser would too)
|
|
219
|
-
const key = body.slice(0, colon).trim();
|
|
220
|
-
const value = body.slice(colon + 1).trim();
|
|
221
|
-
if (block.keyLine !== -1) {
|
|
222
|
-
// We already found our verb; a new key at <= our indent ends the block.
|
|
223
|
-
if (indent <= block.keyIndent)
|
|
224
|
-
break;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (key === verb && indent === 0) {
|
|
228
|
-
block.keyLine = i;
|
|
229
|
-
block.keyIndent = indent;
|
|
230
|
-
if (value === '[]')
|
|
231
|
-
block.inlineEmpty = true;
|
|
232
|
-
else if (value !== '')
|
|
233
|
-
block.inlineScalarValue = value;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (block.itemIndent === -1)
|
|
237
|
-
block.itemIndent = block.keyIndent + 2;
|
|
238
|
-
return block;
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Parse the items currently under a verb block into ParsedEntry form, for
|
|
242
|
-
* matching. Handles list items and the inline-scalar form.
|
|
243
|
-
*/
|
|
244
|
-
function blockEntries(fm, block) {
|
|
245
|
-
const out = [];
|
|
246
|
-
if (block.inlineScalarValue !== null) {
|
|
247
|
-
const r = parseAccessEntry(block.inlineScalarValue);
|
|
248
|
-
if (r.ok)
|
|
249
|
-
out.push({ entry: r.entry, line: block.keyLine });
|
|
250
|
-
return out;
|
|
251
|
-
}
|
|
252
|
-
for (const li of block.itemLines) {
|
|
253
|
-
const content = uncomment(fm[li]).trim();
|
|
254
|
-
const value = content === '-' ? '' : content.slice(2).trim();
|
|
255
|
-
if (!value)
|
|
256
|
-
continue;
|
|
257
|
-
const r = parseAccessEntry(value);
|
|
258
|
-
if (r.ok)
|
|
259
|
-
out.push({ entry: r.entry, line: li });
|
|
260
|
-
}
|
|
261
|
-
return out;
|
|
262
|
-
}
|
|
263
|
-
// ---------------------------------------------------------------------------
|
|
264
|
-
// Public splice operations
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
/**
|
|
267
|
-
* Add `principal` under `verb`. Idempotent: if an entry with the EXACT same
|
|
268
|
-
* token spelling already exists with the SAME deny-ness (a grant when
|
|
269
|
-
* granting, a deny when denying), returns `changed: false`. Idempotency is
|
|
270
|
-
* deliberately exact-token, never name-level: a bare token and a `role/`
|
|
271
|
-
* token are different principals (group-first precedence gives the bare key
|
|
272
|
-
* to a same-named group), so a bare-`X` GROUP grant must go through even when
|
|
273
|
-
* `role/X` is present, and vice versa. Creates the verb key (and the
|
|
274
|
-
* frontmatter fence, for a fresh file) as needed. Preserves everything else
|
|
275
|
-
* byte-for-byte.
|
|
276
|
-
*
|
|
277
|
-
* `allowScalar` controls whether a fresh single-entry verb may be written in
|
|
278
|
-
* the node-frontmatter scalar form (`owner: Name <email>`). access.md always
|
|
279
|
-
* uses the block-list form; node frontmatter prefers scalar for a lone entry.
|
|
280
|
-
*
|
|
281
|
-
* `deny` writes the entry as a `deny <principal>` denial instead of a grant
|
|
282
|
-
* (the `deny-here` per-item override). The block/scalar/empty-list handling is
|
|
283
|
-
* identical — only the rendered line and the idempotency check differ — so the
|
|
284
|
-
* deny path reuses this engine rather than a parallel `spliceDeny`. NOTE: the
|
|
285
|
-
* resolver lets a same-scope grant override a same-scope deny of the same
|
|
286
|
-
* principal, so a caller adding a deny where a grant already exists must strip
|
|
287
|
-
* that grant first (see `AccessMutationService`); this function only writes the
|
|
288
|
-
* one entry it's asked to.
|
|
289
|
-
*/
|
|
290
|
-
export function spliceGrant(text, verb, rawPrincipal, opts = {}) {
|
|
291
|
-
const principal = validatePrincipal(rawPrincipal);
|
|
292
|
-
const deny = opts.deny ?? false;
|
|
293
|
-
const f = splitFrontmatter(text);
|
|
294
|
-
if (!f.hasFrontmatter) {
|
|
295
|
-
// Fresh file — synthesise a minimal frontmatter block, keep any body lines
|
|
296
|
-
// the caller passed (usually none for a new access.md).
|
|
297
|
-
const itemLine = opts.allowScalar
|
|
298
|
-
? `${verb}: ${renderEntry(principal, deny)}`
|
|
299
|
-
: `${verb}:${f.eol} - ${renderEntry(principal, deny)}`;
|
|
300
|
-
const fenced = ['---', itemLine, '---'].join(f.eol);
|
|
301
|
-
const bodyText = f.post.length ? f.post.join(f.eol) : '';
|
|
302
|
-
const text2 = bodyText ? `${fenced}${f.eol}${bodyText}` : `${fenced}${f.eol}`;
|
|
303
|
-
return { text: text2, changed: true };
|
|
304
|
-
}
|
|
305
|
-
const region = pickRegion(text, f, opts.target ?? 'node');
|
|
306
|
-
const block = findVerbBlock(region.lines, verb);
|
|
307
|
-
const existing = blockEntries(region.lines, block);
|
|
308
|
-
// Idempotency: an entry for this EXACT token with the same deny-ness is
|
|
309
|
-
// already present (a grant when granting, a deny when denying). Exact-token
|
|
310
|
-
// on purpose — see the doc above.
|
|
311
|
-
if (existing.some((e) => entryMatches(e.entry, principal, 'exact') && e.entry.deny === deny)) {
|
|
312
|
-
return { text, changed: false };
|
|
313
|
-
}
|
|
314
|
-
const itemValue = renderEntry(principal, deny);
|
|
315
|
-
if (block.keyLine === -1) {
|
|
316
|
-
// Verb key absent — append a new block at the end of the region.
|
|
317
|
-
const lines = opts.allowScalar ? [`${verb}: ${itemValue}`] : [`${verb}:`, ` - ${itemValue}`];
|
|
318
|
-
region.lines.push(...lines);
|
|
319
|
-
return { text: region.join(), changed: true };
|
|
320
|
-
}
|
|
321
|
-
if (block.inlineScalarValue !== null) {
|
|
322
|
-
// Scalar form — promote to a block list with both the old and new entry.
|
|
323
|
-
const old = block.inlineScalarValue;
|
|
324
|
-
region.lines.splice(block.keyLine, 1, `${verb}:`, ` - ${old}`, ` - ${itemValue}`);
|
|
325
|
-
return { text: region.join(), changed: true };
|
|
326
|
-
}
|
|
327
|
-
// Block-list (possibly inline `[]`). Insert after the last existing item, or
|
|
328
|
-
// right after the key line when the list is currently empty.
|
|
329
|
-
const indent = ' '.repeat(block.itemIndent);
|
|
330
|
-
const newLine = `${indent}- ${itemValue}`;
|
|
331
|
-
if (block.inlineEmpty) {
|
|
332
|
-
// `verb: []` -> `verb:` + one item.
|
|
333
|
-
region.lines[block.keyLine] = `${' '.repeat(block.keyIndent)}${verb}:`;
|
|
334
|
-
region.lines.splice(block.keyLine + 1, 0, newLine);
|
|
335
|
-
return { text: region.join(), changed: true };
|
|
336
|
-
}
|
|
337
|
-
const insertAt = block.itemLines.length > 0 ? block.itemLines[block.itemLines.length - 1] + 1 : block.keyLine + 1;
|
|
338
|
-
region.lines.splice(insertAt, 0, newLine);
|
|
339
|
-
return { text: region.join(), changed: true };
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Remove every entry naming `principal` under `verb` (grant or deny). Idempotent:
|
|
343
|
-
* returns `changed: false` when the principal isn't present. If removing the
|
|
344
|
-
* last item empties the verb, the `verb:` key is collapsed to `verb: []` so the
|
|
345
|
-
* file still parses cleanly (the resolver reads `[]` as an empty list).
|
|
346
|
-
* Preserves everything else.
|
|
347
|
-
*
|
|
348
|
-
* Token matching for role-shaped principals is `tokenMatch` (default `'name'`:
|
|
349
|
-
* BOTH spellings — revoking the role removes legacy bare spellings too).
|
|
350
|
-
* Callers pass `'exact'` when a GROUP shadows the principal's bare name: the
|
|
351
|
-
* bare token then belongs to the group and the `role/` token to the role, so a
|
|
352
|
-
* revoke of one must never strip the other (see AccessMutationService, which
|
|
353
|
-
* derives the flag from the merged principal index).
|
|
354
|
-
*/
|
|
355
|
-
export function spliceRevoke(text, verb, rawPrincipal, opts = {}) {
|
|
356
|
-
const principal = validatePrincipal(rawPrincipal);
|
|
357
|
-
const tokenMatch = opts.tokenMatch ?? 'name';
|
|
358
|
-
const f = splitFrontmatter(text);
|
|
359
|
-
if (!f.hasFrontmatter)
|
|
360
|
-
return { text, changed: false };
|
|
361
|
-
const region = pickRegion(text, f, opts.target ?? 'node');
|
|
362
|
-
const block = findVerbBlock(region.lines, verb);
|
|
363
|
-
if (block.keyLine === -1)
|
|
364
|
-
return { text, changed: false };
|
|
365
|
-
if (block.inlineScalarValue !== null) {
|
|
366
|
-
const r = parseAccessEntry(block.inlineScalarValue);
|
|
367
|
-
if (r.ok && entryMatches(r.entry, principal, tokenMatch)) {
|
|
368
|
-
// Lone scalar entry removed -> collapse to empty list.
|
|
369
|
-
region.lines[block.keyLine] = `${' '.repeat(block.keyIndent)}${verb}: []`;
|
|
370
|
-
return { text: region.join(), changed: true };
|
|
371
|
-
}
|
|
372
|
-
return { text, changed: false };
|
|
373
|
-
}
|
|
374
|
-
const entries = blockEntries(region.lines, block);
|
|
375
|
-
const victims = entries.filter((e) => entryMatches(e.entry, principal, tokenMatch)).map((e) => e.line);
|
|
376
|
-
if (victims.length === 0)
|
|
377
|
-
return { text, changed: false };
|
|
378
|
-
// Delete from the bottom up so earlier indices stay valid.
|
|
379
|
-
const remaining = block.itemLines.filter((l) => !victims.includes(l));
|
|
380
|
-
for (const line of [...victims].sort((a, b) => b - a))
|
|
381
|
-
region.lines.splice(line, 1);
|
|
382
|
-
if (remaining.length === 0) {
|
|
383
|
-
// Verb block is now empty — collapse `verb:` to `verb: []`. The key line
|
|
384
|
-
// index is unchanged because we only removed lines that came after it.
|
|
385
|
-
region.lines[block.keyLine] = `${' '.repeat(block.keyIndent)}${verb}: []`;
|
|
386
|
-
}
|
|
387
|
-
return { text: region.join(), changed: true };
|
|
388
|
-
}
|
|
389
|
-
//# sourceMappingURL=access-splice.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"access-splice.js","sourceRoot":"","sources":["../../../src/modules/access/access-splice.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Group files — the "who you are" half of the roles/groups split.
|
|
3
|
-
*
|
|
4
|
-
* Groups are people-sets referenced by access grants ("GTM Team can read the
|
|
5
|
-
* battlecards"). A deployment has exactly ONE group source at a time — a mode
|
|
6
|
-
* derived from which file exists, so it holds at any git ref:
|
|
7
|
-
*
|
|
8
|
-
* - `synced-groups.yaml` EXISTS → IdP mode. The file is MACHINE-OWNED,
|
|
9
|
-
* regenerated wholesale from the SCIM directory mirror; membership is
|
|
10
|
-
* managed in the IdP. `groups.yaml` is ignored entirely.
|
|
11
|
-
* - otherwise → manual mode. `groups.yaml` (UI-edited)
|
|
12
|
-
* is the source.
|
|
13
|
-
*
|
|
14
|
-
* Both files share one format, the group twin of roles.yaml:
|
|
15
|
-
*
|
|
16
|
-
* groups:
|
|
17
|
-
* Engineering:
|
|
18
|
-
* - ada@x.io
|
|
19
|
-
* - bo@x.io
|
|
20
|
-
* Empty Group: []
|
|
21
|
-
*
|
|
22
|
-
* Parsing is FORGIVING at the entry level by design — a malformed email or a
|
|
23
|
-
* reserved/duplicate name skips that entry with a warning, it never takes the
|
|
24
|
-
* whole file down. Only structural failures (unparsable YAML, `groups:` not a
|
|
25
|
-
* mapping) reject the file, and even then the RESOLVER degrades to "this file
|
|
26
|
-
* contributes nothing" rather than throwing: unlike roles.yaml there is no
|
|
27
|
-
* admin-lockout risk here, so groups must never be able to brick access
|
|
28
|
-
* resolution. Strict acceptance for WRITES is the job of the write-side
|
|
29
|
-
* validator (`validateGroupsFile` below), which treats warnings as refusals.
|
|
30
|
-
*/
|
|
31
|
-
export declare const GROUPS_YAML = "groups.yaml";
|
|
32
|
-
export declare const SYNCED_GROUPS_YAML = "synced-groups.yaml";
|
|
33
|
-
/**
|
|
34
|
-
* THE name-safety predicate for principal display names — the single source
|
|
35
|
-
* of truth every surface derives from (group creation/rename, IdP sync
|
|
36
|
-
* materialization; thin assert wrappers sit on top). Returns a human-readable
|
|
37
|
-
* reason the name is unsafe, or null when it is fine.
|
|
38
|
-
*
|
|
39
|
-
* The character rules keep the emitted YAML parseable and the entry grammar
|
|
40
|
-
* unambiguous:
|
|
41
|
-
* `:` mis-tokenises as a nested mapping key
|
|
42
|
-
* `#` truncated as a comment by the subset parser's stripComment
|
|
43
|
-
* `<` / `>` collide with the `Name <email>` user-reference shape
|
|
44
|
-
* leading - tokenises as a list item
|
|
45
|
-
* \x00-\x1f control chars / newlines break line structure outright
|
|
46
|
-
* `role/…` reserved: that spelling is the EXPLICIT role token in access
|
|
47
|
-
* entries — a group carrying it could never be referenced.
|
|
48
|
-
*/
|
|
49
|
-
export declare function unsafeNameReason(displayName: string): string | null;
|
|
50
|
-
/** One parsed group: display name + canonicalised member emails. */
|
|
51
|
-
export interface GroupDefinition {
|
|
52
|
-
displayName: string;
|
|
53
|
-
emails: Set<string>;
|
|
54
|
-
}
|
|
55
|
-
/** Canonical group name → definition, in file order. */
|
|
56
|
-
export type GroupsIndex = Map<string, GroupDefinition>;
|
|
57
|
-
export interface ParsedGroupsFile {
|
|
58
|
-
ok: true;
|
|
59
|
-
groups: GroupsIndex;
|
|
60
|
-
/** Entry-level problems that were skipped over (bad email, dup name, …). */
|
|
61
|
-
warnings: string[];
|
|
62
|
-
}
|
|
63
|
-
export interface GroupsFileError {
|
|
64
|
-
ok: false;
|
|
65
|
-
errors: string[];
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Parse a group file (either of the two — same grammar). See the module note
|
|
69
|
-
* for the forgiving-vs-structural split.
|
|
70
|
-
*/
|
|
71
|
-
export declare function parseGroupsFile(text: string, filename: string): ParsedGroupsFile | GroupsFileError;
|
|
72
|
-
/**
|
|
73
|
-
* Write-gate twin of {@link parseGroupsFile}: strict — structural errors AND
|
|
74
|
-
* entry-level warnings both refuse the candidate, so a UI write can never land
|
|
75
|
-
* a group the resolver would silently skip.
|
|
76
|
-
*/
|
|
77
|
-
export declare function validateGroupsFile(text: string, filename: string): {
|
|
78
|
-
ok: true;
|
|
79
|
-
} | {
|
|
80
|
-
ok: false;
|
|
81
|
-
errors: string[];
|
|
82
|
-
};
|
|
83
|
-
//# sourceMappingURL=group-files.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"group-files.d.ts","sourceRoot":"","sources":["../../../src/modules/access/group-files.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWnE;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrB;AAED,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,WAAW,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,gBAAgB,GAAG,eAAe,CAyFpC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAWhD"}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { EMAIL_REGEX, GROUP_REF_PREFIX, RESERVED_ROLE_NAMES, ROLE_TOKEN_PREFIX, canonicalEmail, canonicalRoleName, parseYamlSubset, } from './access-control.service.js';
|
|
2
|
-
/**
|
|
3
|
-
* Group files — the "who you are" half of the roles/groups split.
|
|
4
|
-
*
|
|
5
|
-
* Groups are people-sets referenced by access grants ("GTM Team can read the
|
|
6
|
-
* battlecards"). A deployment has exactly ONE group source at a time — a mode
|
|
7
|
-
* derived from which file exists, so it holds at any git ref:
|
|
8
|
-
*
|
|
9
|
-
* - `synced-groups.yaml` EXISTS → IdP mode. The file is MACHINE-OWNED,
|
|
10
|
-
* regenerated wholesale from the SCIM directory mirror; membership is
|
|
11
|
-
* managed in the IdP. `groups.yaml` is ignored entirely.
|
|
12
|
-
* - otherwise → manual mode. `groups.yaml` (UI-edited)
|
|
13
|
-
* is the source.
|
|
14
|
-
*
|
|
15
|
-
* Both files share one format, the group twin of roles.yaml:
|
|
16
|
-
*
|
|
17
|
-
* groups:
|
|
18
|
-
* Engineering:
|
|
19
|
-
* - ada@x.io
|
|
20
|
-
* - bo@x.io
|
|
21
|
-
* Empty Group: []
|
|
22
|
-
*
|
|
23
|
-
* Parsing is FORGIVING at the entry level by design — a malformed email or a
|
|
24
|
-
* reserved/duplicate name skips that entry with a warning, it never takes the
|
|
25
|
-
* whole file down. Only structural failures (unparsable YAML, `groups:` not a
|
|
26
|
-
* mapping) reject the file, and even then the RESOLVER degrades to "this file
|
|
27
|
-
* contributes nothing" rather than throwing: unlike roles.yaml there is no
|
|
28
|
-
* admin-lockout risk here, so groups must never be able to brick access
|
|
29
|
-
* resolution. Strict acceptance for WRITES is the job of the write-side
|
|
30
|
-
* validator (`validateGroupsFile` below), which treats warnings as refusals.
|
|
31
|
-
*/
|
|
32
|
-
export const GROUPS_YAML = 'groups.yaml';
|
|
33
|
-
export const SYNCED_GROUPS_YAML = 'synced-groups.yaml';
|
|
34
|
-
/**
|
|
35
|
-
* THE name-safety predicate for principal display names — the single source
|
|
36
|
-
* of truth every surface derives from (group creation/rename, IdP sync
|
|
37
|
-
* materialization; thin assert wrappers sit on top). Returns a human-readable
|
|
38
|
-
* reason the name is unsafe, or null when it is fine.
|
|
39
|
-
*
|
|
40
|
-
* The character rules keep the emitted YAML parseable and the entry grammar
|
|
41
|
-
* unambiguous:
|
|
42
|
-
* `:` mis-tokenises as a nested mapping key
|
|
43
|
-
* `#` truncated as a comment by the subset parser's stripComment
|
|
44
|
-
* `<` / `>` collide with the `Name <email>` user-reference shape
|
|
45
|
-
* leading - tokenises as a list item
|
|
46
|
-
* \x00-\x1f control chars / newlines break line structure outright
|
|
47
|
-
* `role/…` reserved: that spelling is the EXPLICIT role token in access
|
|
48
|
-
* entries — a group carrying it could never be referenced.
|
|
49
|
-
*/
|
|
50
|
-
export function unsafeNameReason(displayName) {
|
|
51
|
-
const trimmed = displayName.trim();
|
|
52
|
-
if (!trimmed)
|
|
53
|
-
return 'empty name';
|
|
54
|
-
if (/[:#<>]/.test(trimmed))
|
|
55
|
-
return "contains ':', '#', '<', or '>'";
|
|
56
|
-
// eslint-disable-next-line no-control-regex
|
|
57
|
-
if (/[\x00-\x1f]/.test(trimmed))
|
|
58
|
-
return 'contains control characters';
|
|
59
|
-
if (trimmed.startsWith('-'))
|
|
60
|
-
return "starts with '-'";
|
|
61
|
-
if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
|
|
62
|
-
return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Parse a group file (either of the two — same grammar). See the module note
|
|
68
|
-
* for the forgiving-vs-structural split.
|
|
69
|
-
*/
|
|
70
|
-
export function parseGroupsFile(text, filename) {
|
|
71
|
-
// Tolerate empty keys at parse: this file's contract is ENTRY-level
|
|
72
|
-
// forgiveness (a blank group name is skipped with a warning below), and a
|
|
73
|
-
// hard tokenizer error here would retire every OTHER group fail-closed.
|
|
74
|
-
const parsed = parseYamlSubset(text, { tolerateEmptyKeys: true });
|
|
75
|
-
if (!parsed.ok)
|
|
76
|
-
return { ok: false, errors: [`${filename}: ${parsed.error}`] };
|
|
77
|
-
const root = parsed.value;
|
|
78
|
-
if (root == null || typeof root !== 'object' || Array.isArray(root)) {
|
|
79
|
-
return { ok: false, errors: [`${filename}: must be a top-level mapping`] };
|
|
80
|
-
}
|
|
81
|
-
const groupsNode = root.groups;
|
|
82
|
-
// An empty file (or one with no `groups:` key yet) is a valid empty set —
|
|
83
|
-
// a fresh deployment's groups.yaml starts this way.
|
|
84
|
-
if (groupsNode == null)
|
|
85
|
-
return { ok: true, groups: new Map(), warnings: [] };
|
|
86
|
-
if (typeof groupsNode !== 'object' || Array.isArray(groupsNode)) {
|
|
87
|
-
return { ok: false, errors: [`${filename}: 'groups' must be a mapping`] };
|
|
88
|
-
}
|
|
89
|
-
const groups = new Map();
|
|
90
|
-
const warnings = [];
|
|
91
|
-
for (const [displayName, value] of Object.entries(groupsNode)) {
|
|
92
|
-
const canonical = canonicalRoleName(displayName);
|
|
93
|
-
if (!canonical) {
|
|
94
|
-
warnings.push(`${filename}: empty group name — skipped`);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
// THE shared name-safety predicate (see `unsafeNameReason` above) — the
|
|
98
|
-
// same rules every write surface asserts. The parser must apply it too:
|
|
99
|
-
// a name the writers refuse (control chars, `<`/`>`, a leading `-`, the
|
|
100
|
-
// reserved `role/` prefix) can still reach a group file by hand edit or
|
|
101
|
-
// through a skewed writer, and accepting it here would let the resolver
|
|
102
|
-
// honor — and `validateGroupsFile` pass — a name no editor can ever
|
|
103
|
-
// produce or reference safely.
|
|
104
|
-
const unsafe = unsafeNameReason(displayName);
|
|
105
|
-
if (unsafe) {
|
|
106
|
-
warnings.push(`${filename}: group ${JSON.stringify(displayName)} ${unsafe} — skipped`);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (RESERVED_ROLE_NAMES.has(canonical)) {
|
|
110
|
-
warnings.push(`${filename}: group '${displayName}' uses reserved name '${canonical}' — skipped`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (groups.has(canonical)) {
|
|
114
|
-
warnings.push(`${filename}: group '${displayName}' canonicalises to '${canonical}', already declared as '${groups.get(canonical).displayName}' — skipped`);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (value !== null && !Array.isArray(value)) {
|
|
118
|
-
warnings.push(`${filename}: group '${displayName}' must be a list of emails — skipped`);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const emails = new Set();
|
|
122
|
-
for (const raw of value ?? []) {
|
|
123
|
-
if (typeof raw !== 'string') {
|
|
124
|
-
warnings.push(`${filename}: group '${displayName}' has a non-string entry — entry skipped`);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const email = canonicalEmail(raw);
|
|
128
|
-
// The reserved `group:` prefix would PASS the email regex
|
|
129
|
-
// (`group:lee@x.io` shapes like an email) but it is the roles.yaml
|
|
130
|
-
// group-reference token, not an address — groups contain emails, never
|
|
131
|
-
// other groups. Skip it with its own warning so the write gate refuses
|
|
132
|
-
// it and the resolver never grants a colon-bearing "email".
|
|
133
|
-
if (email.startsWith(GROUP_REF_PREFIX)) {
|
|
134
|
-
warnings.push(`${filename}: group '${displayName}' has a '${GROUP_REF_PREFIX}'-prefixed entry ${JSON.stringify(raw)} — groups contain emails, not group references; entry skipped`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (!EMAIL_REGEX.test(email)) {
|
|
138
|
-
warnings.push(`${filename}: group '${displayName}' has malformed email '${raw}' — entry skipped`);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
emails.add(email);
|
|
142
|
-
}
|
|
143
|
-
groups.set(canonical, { displayName: displayName.trim(), emails });
|
|
144
|
-
}
|
|
145
|
-
return { ok: true, groups, warnings };
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Write-gate twin of {@link parseGroupsFile}: strict — structural errors AND
|
|
149
|
-
* entry-level warnings both refuse the candidate, so a UI write can never land
|
|
150
|
-
* a group the resolver would silently skip.
|
|
151
|
-
*/
|
|
152
|
-
export function validateGroupsFile(text, filename) {
|
|
153
|
-
// Strict PARSE first (no empty-key tolerance): the read side forgives a
|
|
154
|
-
// blank key entry-level so other groups keep resolving, but a WRITE that
|
|
155
|
-
// contains one — anywhere, including outside the `groups:` mapping — must
|
|
156
|
-
// refuse rather than land bytes the tolerant reader silently drops.
|
|
157
|
-
const strict = parseYamlSubset(text.trim() ? text : 'groups:');
|
|
158
|
-
if (!strict.ok)
|
|
159
|
-
return { ok: false, errors: [`${filename}: ${strict.error}`] };
|
|
160
|
-
const parsed = parseGroupsFile(text, filename);
|
|
161
|
-
if (!parsed.ok)
|
|
162
|
-
return { ok: false, errors: parsed.errors };
|
|
163
|
-
if (parsed.warnings.length > 0)
|
|
164
|
-
return { ok: false, errors: parsed.warnings };
|
|
165
|
-
return { ok: true };
|
|
166
|
-
}
|
|
167
|
-
//# sourceMappingURL=group-files.js.map
|