Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08bb4d827d | ||
|
|
508ee4d61f |
@@ -0,0 +1,64 @@
|
||||
# Row 193a build: the viewer level on R1's access file
|
||||
|
||||
Morgana (Opus 5.5), 2026-10-20. Gitea #1691, parent row 193, design `agents/morgana/work/queue-193/DESIGN.md`
|
||||
r5 (e2d161e2e on morgana/row193, approved at r4 by Duckworth, duckworth-0014). Go: sage-1373 item 3. Base:
|
||||
`refactor` at aa1bd209e. Lands after FED R1 and is rebased onto R1's landing.
|
||||
|
||||
## What it is
|
||||
|
||||
Design 4.2: R1 refuses any grant level but `collaborator`. Row 193a widens that check to `collaborator` or
|
||||
`viewer` and refuses anything else. `covers` needs no change: FED 3.5 already compares the grant's level
|
||||
with the one asked for, so a viewer grant matches `{}` and `{level: 'viewer'}`, and fails
|
||||
`{level: 'collaborator'}`.
|
||||
|
||||
## Why the candidate holds only a test now
|
||||
|
||||
R1 (`packages/business/src/access.mjs`) has no candidate yet. Sage ruled no stub on refactor (sage-1373
|
||||
item 3). So this branch carries:
|
||||
|
||||
- `packages/business/tests/access-levels.test.mjs`: the row 193a tests, written against R1's interface as
|
||||
FED r4.5 (acd5f1f42) sections 3.5 and 7 give it.
|
||||
- The source change, below, to apply on R1's candidate at rebase.
|
||||
|
||||
Until R1 lands, the test file fails with `ERR_MODULE_NOT_FOUND` for `../src/access.mjs`. That is expected.
|
||||
|
||||
## The source change for the rebase
|
||||
|
||||
In R1's level check, the allowed set becomes `["collaborator", "viewer"]`. It is one line wherever R1 keeps
|
||||
the level rule. Its refusal message must name the key path `grants[<i>].level`, as R1's own rule does.
|
||||
|
||||
## R1's interface (confirmed, gizmoduck-0029)
|
||||
|
||||
FED r4.5 names `covers(grant, {business, project, instance}, now, {level} = {})` exactly. Gizmoduck
|
||||
confirmed the validator before R1's manifest went to Duckworth:
|
||||
|
||||
- `checkAccess(doc, {providers, peers, businesses, ssoSubjects})` is pure, returns the checked document
|
||||
frozen, and throws a `BusinessError` (exit 2) naming the key path on any failure;
|
||||
- `businesses` maps a business id to `{human, instances}`;
|
||||
- `ssoSubjects` is a required array of `{provider, sub}` from `console.sso.subjects`; the test passes `[]`;
|
||||
- a grant with no `level` refuses; an expired grant passes `checkAccess` and fails `covers`.
|
||||
|
||||
Every call into the validator goes through one `check()` helper at the top of the test file, so a different
|
||||
name or shape changes one place at rebase. The `covers` calls follow FED exactly.
|
||||
|
||||
A grant with no `level` refuses (`undefined` in the refused list), which R1 also does.
|
||||
|
||||
## Evidence
|
||||
|
||||
Run on 2026-10-20 in the row193a worktree with `node --test --test-reporter=tap
|
||||
tests/access-levels.test.mjs` from `packages/business`.
|
||||
|
||||
- Base (no access.mjs): fails to load, `ERR_MODULE_NOT_FOUND`.
|
||||
- Throwaway reference: an `access.mjs` with only the level rule, a declared-business rule and FED's
|
||||
`covers`, placed in `src/`, run, then deleted, never committed. Result: pass 4, fail 0.
|
||||
|
||||
Mutants on that reference, each failing exactly one test:
|
||||
|
||||
| Mutant | Failing test |
|
||||
|---|---|
|
||||
| levels back to collaborator only | 1, "a grant's level is collaborator or viewer" |
|
||||
| `owner` added to the levels | 2, "any other level refuses with the key path named" |
|
||||
| `covers` ignores the asked level | 3, "covers tells a viewer grant from a collaborator grant" |
|
||||
| `covers` passes any grant when collaborator is asked | 3 |
|
||||
|
||||
At rebase, the same four mutants run again on R1's real code before the row goes to review.
|
||||
@@ -0,0 +1,74 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { BusinessError } from "../src/errors.mjs";
|
||||
import * as access from "../src/access.mjs";
|
||||
|
||||
// Row 193a (#1691): the viewer level. R1 (federation design r4.5, section 7) owns access.mjs and its
|
||||
// tests; this file pins only what row 193 adds: a grant's level is collaborator or viewer, and covers()
|
||||
// tells the two apart. Every call into R1 goes through check() below, so a change to R1's loader
|
||||
// interface edits one place.
|
||||
const CONTEXT = {
|
||||
providers: ["authentik"],
|
||||
peers: ["factory"],
|
||||
businesses: { "creative-hub": { human: "melanie", instances: ["researcher", "writer"] } },
|
||||
ssoSubjects: [],
|
||||
};
|
||||
const check = (doc) => access.checkAccess(doc, CONTEXT);
|
||||
|
||||
const PROJECT = "0b6f4a52-6f1e-4c7a-9a0e-3f1d2c4b5a69";
|
||||
const NOW = new Date("2026-10-20T19:30:00Z");
|
||||
|
||||
function file(level) {
|
||||
const grant = { id: "g-1", user: "jason", business: "creative-hub", projects: ["*"], instances: ["researcher"] };
|
||||
if (level !== undefined) grant.level = level;
|
||||
return {
|
||||
accessVersion: 1,
|
||||
revision: 1,
|
||||
users: [{ id: "jason", home: "local", subjects: [{ provider: "authentik", sub: "s-1" }] }],
|
||||
grants: [grant],
|
||||
};
|
||||
}
|
||||
|
||||
function refuses(fn, pattern) {
|
||||
assert.throws(fn, (error) => {
|
||||
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
|
||||
assert.equal(error.exitCode, 2);
|
||||
assert.match(error.message, pattern);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
test("a grant's level is collaborator or viewer", () => {
|
||||
for (const level of ["collaborator", "viewer"]) {
|
||||
assert.doesNotThrow(() => check(file(level)), level);
|
||||
}
|
||||
});
|
||||
|
||||
test("any other level refuses with the key path named", () => {
|
||||
for (const level of ["owner", "admin", "member", "Viewer", "viewer ", "", null, 1, ["viewer"], undefined]) {
|
||||
refuses(() => check(file(level)), /grants\[0\]\.level/);
|
||||
}
|
||||
});
|
||||
|
||||
test("covers tells a viewer grant from a collaborator grant", () => {
|
||||
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
|
||||
const viewer = file("viewer").grants[0];
|
||||
const collaborator = file("collaborator").grants[0];
|
||||
// Share reads ask for any level (design 9); create, open, send and share ask for collaborator.
|
||||
assert.equal(access.covers(viewer, target, NOW, {}), true);
|
||||
assert.equal(access.covers(viewer, target, NOW, { level: "viewer" }), true);
|
||||
assert.equal(access.covers(viewer, target, NOW, { level: "collaborator" }), false);
|
||||
assert.equal(access.covers(collaborator, target, NOW, {}), true);
|
||||
assert.equal(access.covers(collaborator, target, NOW, { level: "collaborator" }), true);
|
||||
assert.equal(access.covers(collaborator, target, NOW, { level: "viewer" }), false);
|
||||
});
|
||||
|
||||
test("a viewer grant still needs its business, project, instance and expiry to cover", () => {
|
||||
const viewer = { ...file("viewer").grants[0], projects: [PROJECT], expires: "2026-10-21T00:00:00Z" };
|
||||
const target = { business: "creative-hub", project: PROJECT, instance: "researcher" };
|
||||
assert.equal(access.covers(viewer, target, NOW, {}), true);
|
||||
assert.equal(access.covers(viewer, { ...target, business: "other" }, NOW, {}), false);
|
||||
assert.equal(access.covers(viewer, { ...target, project: "7c1e2d3f-0a9b-4c8d-8e7f-6a5b4c3d2e1f" }, NOW, {}), false);
|
||||
assert.equal(access.covers(viewer, { ...target, instance: "writer" }, NOW, {}), false);
|
||||
assert.equal(access.covers(viewer, target, new Date("2026-10-21T00:00:01Z"), {}), false);
|
||||
});
|
||||
Reference in New Issue
Block a user