M1-49: Who has access, preview and view as¶
Status: ready | Depends on: M1-48 | Milestone: M1
Goal¶
After this ticket the module authz answers the three questions an owner or a person with manage
asks around a permission change: who can access a space or channel and why (access.explain),
what a grant would change before it exists (access.preview, a dry run in the real transaction
with a plain-text explanation from fixed templates in German and English), and what one person
sees (access.view_as, read only, time limited, audited). All three are computed by the same
snapshot code that authorize uses, so they cannot disagree with a real decision. They feed the
console screens A2 to A4 (M6) and carry hint H1 ("read horizon") wherever the specification asks
for it.
Read first¶
docs/PLAN.md, section 7 "M1 Foundation": row M1-49, "Prerequisites from the core contract" (rowaccess.preview... K-RE-7 to K-RE-9), "Test strategy", "Red team"; section 3 "Way of working per ticket".docs/briefs/README.md.docs/spec/07-permissions.md: "3. Principal context" (fieldview_as), "4.1 Decisions and reasons", "4.2 Flow" (steps 4 and 7), "4.3 Visibility", "4.4 Invariants in the transaction", "SR2 Tool class", "SR6 Direct messages only for participants", "6. Read horizon of agents" (H(A), T(A), H1), "7.2 Permission state (snapshot)" (provenance), "9.1 Who has access?", "9.2 View as person", "9.3 Plain-text preview", "10. Examples", "11.1 Test matrix" (world, AZ-M21, AZ-M31, AZ-M37, AZ-M38, AZ-M46, AZ-M47), "11.2 Required tests" (AZ-P8), "12. Decisions" (R-RE-07, R-RE-10), "14. Configuration values".docs/spec/05-actions.md: "1. Binding rules" (rules 1.6, 1.7), "2.3 Effective level", "2.4" (columns), "4.2 Instance and administration" (rowsaccess.explain,access.preview,access.view_as), "4.4 Groups and grants" (grant.create), "8" (R-AZ-05, R-AZ-13).docs/spec/03-data-model.md: tablesgrants,groups,group_memberships,owners,principals,users(locale,time_zone),agents(billing_mode,credential_holder_id,tool_class,reported_tool_class,reported_details); "6.3 Lock order", "6.4 I1 check".docs/spec/10-backend-policy.md: "5.3 Query" (triggerer query), "11. Guided flow" (i1_conflicts,organization_ready).docs/spec/04-sync-protocol.md: "8.2 What happens on a change" (diff format).docs/spec/02-architecture.md: "4.1 Rules" (source ports), "4.3 Ports, events and sources per module", "4.4 Allowed dependencies", "5. Layers within a module", "8.1 Flow" (unit of work and phase hooks), "9.3 Proof of the check".docs/spec/01-product.md: A2, A3, A4 wireframes; "9. Languages" (language followsusers.locale).docs/spec/06-api.md: "5. Errors", "7. Idempotency" (previewGranthas noIdempotency-Key), "16. Endpoints" group "Access", "13. Versioning" (additive changes).docs/api/openapi.yaml: operationsexplainSpaceAccess,explainChannelAccess,previewGrant,viewAsUser,createGrant; schemasAccessExplanation,AccessEntry,AccessSource,AccessTargetKind,GrantCreate,GrantPreview,AccessEffect,InvariantViolation,ViewAsSnapshot,SpaceView,ChannelView,Space,Channel,PrincipalRef,Level,LevelOrNone.- ADR 0017, ADR 0018;
docs/security/threat-model.mdrow T-OW-01. docs/operations/configuration.md:authz.view_as_max_duration,auth.step_up_max_age.- Code already merged:
services/server/internal/authz(M1-27 to M1-30, M1-48: snapshot,authorize, cache,Scope, phase hook withVerifyInvariants, admin port, the fake-source harness and the random-world generator of AZ-P8),internal/kernel/authorize,internal/kernel/actor,internal/kernel/txn,internal/platform/pg/pgtest,services/server/api/operations.yaml. docs/quality/gates.md,CONTRIBUTING.md,docs/quality/red-team-gate.md.
Fixed requirements that apply¶
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Note on rule 3: In this repository docs/api/openapi.yaml is normative and the server interface is
generated from it (ADR 0013); the route test keeps both identical.
- Test first: every behaviour gets a test that fails before the code exists (red), then passes (green). Keep the red output for the report.
- No en dash or em dash characters (U+2013, U+2014) in any file or commit message.
- White label: no company, person, host or domain names except the examples Alex, Sam, Robin,
the agents from 07 "10. Examples" and
example.com; no paths of a real machine. - Limits and durations only through named keys from
docs/operations/configuration.md; no bare numbers in production code or documentation. - Specification first: every API change below lands in
docs/api/openapi.yamland the chapter in the same change, before the code that implements it (PLAN principle 3). - Product requirements implemented here: "Who has access?" with provenance per principal
(07 "9.1"); preview of every permission change as a dry run in the real transaction with
rollback, I1 and SR8 results and hints in plain text from templates (07 "9.3", R-RE-10);
"View as person" only for owners with step-up, intersection of permissions, DM channels
hidden, every write
view_as_read_only, ends afterauthz.view_as_max_duration, audited (07 "9.2", SR6, AZ-M46, AZ-M47); hint H1 in preview and explanation (07 "6").
Scope¶
The specification is in place (planning, spec first): docs/api/openapi.yaml (GrantPreview
with hints, text, text_locale, denial_reason; PreviewHint; InvariantViolation with
credential_holder_id and organization_ready; AccessEntry.level as LevelOrNone and
inactive; AccessSource kinds operator, via_agent, guard_blocked with agent_id, rule,
expires_at, membership_expires_at, reason; ViewAsSnapshot.expires_at; descriptions of the
four operations) and 07 "9.1" (API source kinds), "9.2" (view-as in M1 as a snapshot), "9.3"
(language rule users.locale with fallback de, time zone, catalog in authz, the split between
missing base permission and allowed = false, the example with line 4 unwrapped). Change them
only where the implementation proves them wrong; run make api-lint and make api-checks and
regenerate the server code with the target of M1-17 so that make codegen-check shows no diff.
Code (paths under services/server/):
internal/kernel/actor: addViewAs *ViewAsto the principal context withtype ViewAs struct { UserID ids.ID; StartedAt time.Time }(doc comment: set only byauthzfor an owner session; never parsed from client input in M1).internal/authz(port): extend theAdminport withExplainAccess(ctx, target AccessTargetRef) (AccessExplanation, error),PreviewGrant(ctx, in GrantCreateInput) (GrantPreview, error),ViewAs(ctx, userID ids.ID) (ViewAsSnapshot, error); port DTOs mirror the openapi schemas. Extend the declared source ports where the base branch lacks a capability (keep names of the base; these are the needed capabilities):PrincipalSource: describe principals by ID: kind, handle, display name, status, and for humanslocaleandtime_zone.StructureSource: names of spaces and channels; channels of a space; effective operator per channel and default operator per space; the targets an agent serves (07 "6", 10 "5.2"); the data needed to fill the openapiSpaceandChannelobjects forViewAsSnapshot.AgentPolicySource: per agent status,billing_mode,credential_holder_id, effective tool class (higher oftool_classandreported_tool_class),billing_modes_supported. Update every fake in the authz test harness and the existing real implementations (identity forPrincipalSource; chat and agents only if their adapters exist in the base branch) in this change. A shared contract test per source port runs against fake and real implementations.
internal/authz/domain: provenance mode of the snapshot builder (explain.go), read horizon and trigger set (horizon.go:H(A),T(A)minus principals blocked by SR2, H1 pairs), preview diff toAccessEffect(preview.go), plain-text rendererinternal/authz/domain/previewtextwith the catalog filescatalog_de.goandcatalog_en.go(stdlibtext/templateor plain formatting; no third-party library in domain).internal/authz/app: use casesexplain.go,preview.go,viewas.go;authorizehonorsViewAs(07 "4.2" step 4 intersection, step 7view_as_read_only, expiry rule below) if the base does not already.internal/authz/adapters/postgres: queries for explanation candidates (grants by target on the path, valid group members, owners) and for the trigger set of any agent (generalize the I1 triggerer query of 10 "5.3"; no copy).internal/authz/adapters/httpapi: handlersexplainSpaceAccess,explainChannelAccess,previewGrant,viewAsUser.internal/wire: wiring;services/server/api/operations.yaml: move the four operations fromlaterto the enabled list.- Configuration: register
authz.view_as_max_duration(duration) in the typedauthzconfig and the config schema (M1-07); the chapter row in 07 "14" exists. - Docs:
internal/authz/README.md(ports, new operations, rules),services/server/CHANGELOG.mdunder Unreleased, rulings indocs/DECISIONS.md(list in Design notes, "Rulings to record").
Out of scope¶
- Groups and grants API itself (M1-48, merged), expiry jobs (M1-30),
resolve_i1(not in theGrantCreateschema; preview of it waits for the ticket that adds it). - Previews of other actions with "Permissions" =
yes(07 "9.3": they come with their routes). - The console screens, the "Permission hints" list and console browsing of content as another
person (M6-01 adds the view-as state of the console session with start and end operations to
the core contract first, 07 "9.2"); in M1 only
authzsetsactor.ViewAs, insideviewAsUserand in tests. - SR3 data (dev environments, V2; project bindings, M2):
rule = sr3is defined in the schema but emitted only once those exist. - Spaces, channels and agents tables and their APIs (M1-51 to M1-53).
Boundaries¶
- Work in a dedicated git worktree on branch
feat/M1-49-authz-explain. - Conventional Commits with scope (
feat(authz): ...,docs(api): ...) and DCO sign-off (git commit -s). - Never push, tag, open pull requests or create remote repositories.
- Do not modify files outside the scope above except the CHANGELOG and docs entries named here.
- Follow the host rules supplied by the launcher.
- Stop only processes you started, by PID. Temporary directories only via
mktemp -d; remove them at the end. - Never read or print secrets. No network calls except package downloads for pinned tools.
Design notes¶
Same code as authorize:
- Explanation, preview diff and view-as all use the snapshot builder of M1-28 (07 "7.2"). The
explanation runs it in provenance mode (provenance is never cached). A differential test proves
that the level and capabilities in every
AccessEntryequal whatauthorizederives.
access.explain (explainSpaceAccess, explainChannelAccess):
- Authorization:
authorize(p, access.explain, space|channel): no read gives 404 (same body as an unknown ID), read withoutownerormanagegives 403forbidden(capability_missing). No step-up, no audit, classrl.read. - Candidates: active and inactive owners, subjects of valid and not valid grants on the target's
path (instance, space, and for a space target its channels are not included), valid members of
those groups, the effective operator of a channel target and the default operator of a space
target. For each candidate compute effective level and capabilities on the target with
provenance. Entries with level
noneare dropped unless they carry avia_agentsource. - Sources:
owner;direct(grant on the target itself, withgrant_id,expires_at,reason);group(withgroup_id,grant_id,membership_expires_at,expires_at);inherited(withfrom_target_kind,from_target_id,grant_id; combined withgroupdata when the inherited grant is a group grant);operator(agent is effective operator of the channel, or default operator of the space; levelwriteper R-AZ-13);dm_participant(DM channels; owners never see them, SR6);via_agent(H1: principal P is inT(A)of an agent A whose horizonH(A)contains the channel, and P cannot read it; for a space target: any channel of the space);guard_blocked(P hastriggerbut SR2 blocks triggering the channel's operator A:rule = sr2,agent_id = A). inactive = truefor principals with status other thanactive(their grants do not act, SR7; the entry shows the level the grants would give).- Ordering: owners first, then by display name, then by ID. No pagination (small sets, 06 "6" rule 6).
H(A): channels A can read (grants of A plus operator rule, including channels with a thread whose fixed operator is A).T(A): active humans with effectivetriggeron any target A serves (10 "5.2"), minus those SR2 blocks (A's effective tool class ishostand P is not an owner).
access.preview (previewGrant):
- Authorization:
authorize(p, access.preview, target of the grant)with the requirement ofgrant.create(owner, ormanageon the target), never step-up, never audit, classrl.admin. No read on the target gives 404, read withoutmanagegives 403. The limits of 07 "2.6" (delegation_exceeds_own_level,delegation_exceeds_own_capabilities, subject agent or app by a non-owner or agent and app withtriggeror capabilities other thanupload:subject_kind_not_allowed, instance target by a non-owner:owner_only) do not fail the request: the response is200withallowed = falseanddenial_reason. - Dry run in the real transaction (R-RE-10): structure the
createGrantuse case of M1-48 into an authorization step and an apply step. The preview opens the unit of work with the sameRightsChangeoption ascreateGrant(phase 1 lockspermission_versionand captures the before state), runs the apply step (insert, events and audit records stay buffered), then calls the same evaluator the authz pre-commit hook uses (diff of visible targets, I1, SR8) directly, stores the result and returns a package-private sentinel error so the unit of work rolls back. The sync and audit hooks never run, so noevent_logrow, nopg_notify, no audit row and no counter change survive. The handler maps the sentinel to the preview result. Do not add a dry-run mode tokernel/txn. effects: oneAccessEffectper affected principal and target from the diff (same diff asaccess.changed, 04 "8.2"),before_levelandafter_levelasLevelOrNone,capabilities_added.invariant_violations: per violating personal agentcode = i1_violation,agent_id,principal_ids(the extra triggerers),credential_holder_id,organization_ready,hint("Switch the agent to organization first").allowed = falseif any violation or a denial reason exists.hints(07 "9.3"):trigger_blocked_tool_classper operator agent of the target with effective classhostwhen the subject is not an owner (for group subjects always);read_horizonper (agent A served by the target, channel K inH(A), principal gainingtriggerwho cannot read K after the change);group_reachfor group subjects withcount= active members with a valid membership;expiry_missingfor subject kinduserwithoutexpires_at.
Plain-text rendering (text):
- Language: primary subtag of the caller's
users.locale(01 "9. Languages");deoren, anything else falls back tode(product default).text_localenames the language used. - Dates: the instant
expires_atformatted in the caller'susers.time_zone(invalid zone: UTC);deuses02.01.2006,enuses2006-01-02(Go layouts). - Names: principal display name; group name;
<space>/<channel>for channels. - Lines in this order: summary; abilities; capabilities; then one line per hint in the order denial, I1 violations (by agent name), tool class (by agent name), read horizon (by agent name, space name, channel name), group reach, missing expiry.
- Abilities by level:
read= ability.read;write= ability.write;trigger= ability.write followed by,plus ability.trigger_agent for each active operator agent of the target (channel: its effective operator; space: default operator and operators of its channels; instance: all operators), sorted by display name, excluding agents with an I1 violation and agents blocked by SR2. Agents and apps as subjects never get the trigger part. - Capabilities line: always upload and approve lines;
plus caps.export andplus caps.manage appended only when granted. - Catalog (placeholders in braces; quotes are ASCII):
| Key | de | en |
|---|---|---|
level.read / .write / .trigger |
Lesen / Schreiben / Auslösen |
Read / Write / Trigger |
subject.group |
Gruppe {group} |
Group {group} |
where.instance |
in der ganzen Instanz |
in the whole instance |
where.space |
im Bereich {space} |
in space {space} |
where.channel |
im Kanal {space}/{channel} |
in channel {space}/{channel} |
summary |
{subject} erhält{until} die Stufe "{level}" {where}. |
{subject} gets the level "{level}" {where}{until}. |
until |
bis {date} |
until {date} |
abilities.single |
{subject} kann damit: {abilities}. |
{subject} can then: {abilities}. |
abilities.group |
Mitglieder von {group} können damit: {abilities}. |
Members of {group} can then: {abilities}. |
ability.read |
Nachrichten lesen |
read messages |
ability.write |
Nachrichten lesen und schreiben, Threads anlegen |
read and write messages, create threads |
ability.trigger_agent |
Agent {agent} auslösen |
trigger agent {agent} |
caps |
Dateien hochladen: {upload}. Freigabe-Karten bestätigen: {approve}. |
Upload files: {upload}. Confirm approval cards: {approve}. |
caps.export / caps.manage |
Exportieren: ja. / Verwalten: ja. |
Export: yes. / Manage: yes. |
yes / no |
ja / nein |
yes / no |
denial.delegation_exceeds_own_level |
Abgelehnt: Du kannst höchstens deine eigene Stufe weitergeben. |
Rejected: you can grant at most your own level. |
denial.delegation_exceeds_own_capabilities |
Abgelehnt: Du kannst nur eigene Zusatzrechte weitergeben. |
Rejected: you can only grant capabilities you have yourself. |
denial.subject_kind_not_allowed |
Abgelehnt: Agenten und Apps bekommen Freigaben nur vom Owner, höchstens Schreiben und nur Dateien hochladen. |
Rejected: agents and apps get grants only from an owner, at most Write and only Upload files. |
denial.owner_only |
Abgelehnt: Freigaben auf der ganzen Instanz legt nur ein Owner an. |
Rejected: only an owner grants on the whole instance. |
hint.i1 |
Abgelehnt: {agent} läuft auf dem persönlichen Abo von {holder}. Zuerst auf Organisation umstellen. |
Rejected: {agent} runs on the personal subscription of {holder}. Switch it to organization first. |
hint.tool_class |
Hinweis: {agent} hat die Werkzeugklasse Host; {subject} kann {agent} damit nicht auslösen (nur Owner). |
Note: {agent} has the tool class host; {subject} cannot trigger {agent} (owners only). |
hint.read_horizon |
Hinweis: {agent} liest auch {space}/{channel}. {subject} darf {space}/{channel} nicht lesen, kann aber über {agent} Inhalte daraus erfahren. |
Note: {agent} also reads {space}/{channel}. {subject} may not read {space}/{channel} but can learn its contents through {agent}. |
hint.read_horizon.group (count > 1) |
Hinweis: {agent} liest auch {space}/{channel}. {count} Mitglieder von {group} dürfen {space}/{channel} nicht lesen, können aber über {agent} Inhalte daraus erfahren. |
Note: {agent} also reads {space}/{channel}. {count} members of {group} may not read {space}/{channel} but can learn its contents through {agent}. |
hint.read_horizon.group_one |
Hinweis: {agent} liest auch {space}/{channel}. 1 Mitglied von {group} darf {space}/{channel} nicht lesen, kann aber über {agent} Inhalte daraus erfahren. |
Note: {agent} also reads {space}/{channel}. 1 member of {group} may not read {space}/{channel} but can learn its contents through {agent}. |
hint.group_reach / .one |
Hinweis: Die Gruppe {group} erreicht {count} Menschen. / Hinweis: Die Gruppe {group} erreicht einen Menschen. |
Note: the group {group} reaches {count} people. / Note: the group {group} reaches one person. |
hint.expiry_missing |
Hinweis: Die Freigabe für {subject} hat kein Ablaufdatum. |
Note: the grant for {subject} has no expiry date. |
- Golden file
internal/authz/domain/previewtext/testdata/example_07_de.golden(exactly 4 lines):
Sam erhält bis 31.12.2026 die Stufe "Auslösen" im Kanal Marketing/Kampagnen.
Sam kann damit: Nachrichten lesen und schreiben, Threads anlegen, Agent Texter auslösen.
Dateien hochladen: nein. Freigabe-Karten bestätigen: nein.
Hinweis: Texter liest auch Marketing/Intern. Sam darf Marketing/Intern nicht lesen, kann aber über Texter Inhalte daraus erfahren.
- Golden file
example_07_en.golden:
Sam gets the level "Trigger" in channel Marketing/Kampagnen until 2026-12-31.
Sam can then: read and write messages, create threads, trigger agent Texter.
Upload files: no. Confirm approval cards: no.
Note: Texter also reads Marketing/Intern. Sam may not read Marketing/Intern but can learn its contents through Texter.
- World of both golden tests: owner (locale
deorenrespectively, time zoneUTC); Sam without grants; space Marketing with channels Kampagnen and Intern; agent Texter,organization, tool classchat, default operator of Marketing; grant: Sam, channel Marketing/Kampagnen,trigger, no capabilities,expires_at = 2026-12-31T12:00:00Z, reasonCampaign.
access.view_as (viewAsUser):
authorize(p, access.view_as, user): owner in a session, step-up, audit of allow and deny, classrl.admin. Unknown user or a principal that is not a human gives 404.- The handler sets
actor.ViewAs{UserID, StartedAt: request_time}and computes the visible spaces and channels throughScope/authorizewith that context: effective permissions are the intersection of owner and person (07 "4.2" step 4); DM channels never appear (SR6); archived structure never appears. Each item carries the intersectedlevelandcapabilities. A disabled person yields empty lists.expires_at = request_time + authz.view_as_max_duration. - Audit entry: action
access.view_as, resourceuser, details{"target_user_id": ..., "expires_at": ...}; no names. authorizewithViewAsset: every action whose catalog permission writes ends withforbidden, reasonview_as_read_only(07 "4.2" step 7); oncerequest_time >= StartedAt + authz.view_as_max_durationevery decision isstep_up_required(the view ended; a new one needs a fresh step-up). No new reason code.
Other rules:
- No JSON
nullanywhere; absent optional fields are omitted (ADR 0053). - Every handler proves its
authorizecall (02 "9.3"). - Rulings to record in
docs/DECISIONS.md: language and date rule of the preview text; denial reasons giveallowed = falseinstead of 403; preview rollback by sentinel error without akernel/txnmode; view-as in M1 is a snapshot withactor.ViewAs, expiry asstep_up_required; which capabilities the text always lists.
Tests (write them first)¶
Use the fake-source harness of M1-28/M1-29 for domain and app tests and pgtest (PostgreSQL 18
throwaway container, 127.0.0.1, random port) for integration tests. Test names carry the IDs.
Domain and app (fakes, controllable clock):
TestPreviewTextExample07GermanandTestPreviewTextExample07English: render the world above and compare byte for byte with the golden files.TestPreviewTextLanguageFallback: localefr-FRrenders German,en-GBEnglish.TestPreviewHintToolClassHost: Runner (host) operates Engineering/Werkzeuge; preview of Alextriggerthere has the hint, and Runner is not in the abilities line.TestPreviewHintGroupReach: group with three valid and one expired membership givescount3.TestPreviewHintExpiryMissing: user subject without expiry has the hint, group subject not.TestPreviewAZM21_I1ViolationIsReportedNotVetoed: world of 07 "11.1"; Owner previews Samtriggeron Research/Notizen while Assistent ispersonal:allowed = false, one violation withagent_idAssistent,principal_ids[Sam],credential_holder_idOwner,organization_readyfalse, text linehint.i1.TestPreviewAZM31_AgentTriggerIsDenialNotError,TestPreviewAZM37_DelegationLevel,TestPreviewAZM38_DelegationCapabilities:allowed = falsewith the reason code.TestExplainSources: world of 07 "11.1"; explanation of Support/Allgemein lists Owner (owner), Alex (inheritedfrom G-1), Sam (groupwith membership expiry), Lotse (directG-5 andoperator).TestExplainReadHorizonEntry: Texter reads Marketing/Intern by a grant, Sam hastriggeron Marketing/Kampagnen: explanation of Intern has Sam with levelnoneand sourcevia_agent(Texter).TestExplainGuardBlockedSR2: Alextriggeron Engineering/Werkzeuge: sourceguard_blocked,rule = sr2, agent Runner.TestExplainInactivePrincipalMarked: disabled Robin with a grant appears withinactive = true.TestExplainMatchesAuthorizeInRandomWorlds(property test with the AZ-P8 generator): for every entry, level and capabilities equal thoseauthorizeimplies; every principalauthorizelets read the target appears.TestAZM46_ViewAsAlexCannotReadMarketing: Owner withViewAsAlex,message.readon Marketing/Kampagnen givesnot_found.TestAZM47_ViewAsIsReadOnly: Owner withViewAsAlex,message.createon Support/Allgemein givesforbiddenview_as_read_only.TestViewAsEndsAfterMaxDuration: one tick before the end the intersection decides; atStartedAt + authz.view_as_max_durationthe decision isstep_up_required.TestViewAsHidesDMs(SR6): the DM Alex-Sam is absent from the snapshot of Alex.
Integration (pgtest, real authz and identity, fake chat and agent sources where the tables do
not exist yet):
TestPreviewRollsBackEverything: before and after one preview the row counts ofgrants,event_log,audit_eventsand the values ofevent_seq,audit_seq,permission_versionare equal, and aLISTEN event_logconnection receives no notification.TestPreviewNeedsNoStepUpAndWritesNoAudit: owner with an expired step-up gets200.TestPreviewAuthorization: no read on the target404, read withoutmanage403,manage200.TestExplainAuthorization: same three cases for both explain operations.TestViewAsRequiresOwnerAndStepUp: non-owner403(owner_only), owner without fresh step-up401step_up_required, owner with step-up200; each attempt has an audit entry (allow or deny).TestViewAsSnapshotOfAlex: G-1 world: only Support and its channels, levels and capabilities asauthorizegives them for Alex,expires_atas specified.TestAccessOperationsMatchSpec: every response of the four operations validates against openapi.yaml (response validation of the test build) and contains nonull.
Commands that must pass:
cd services/server && go test -race ./internal/authz/... ./internal/kernel/actor/... ./internal/identity/...
make codegen-check
make api-lint
make check
Acceptance criteria¶
- The implementation matches
openapi.yamland 07 "9" (changed only where proven wrong);make api-lint,make api-checks,make codegen-checkare green. - The report names what only fakes prove; the integration test with real spaces, channels and agents follows in M1-54 (PLAN row M1-54).
- The example from 07 "9.3" is reproducible byte for byte (German and English golden tests).
- The preview runs in the real transaction and leaves no row, counter or notification behind.
- AZ-M46 and AZ-M47 are green; view-as ends after
authz.view_as_max_duration; everyviewAsUsercall has an audit entry. - Hint H1 appears in preview (
read_horizon) and explanation (via_agent). - The differential test explanation versus
authorizeis green. - The four operations are in the enabled list, registered with their actions (AZ-P1 stays green), and 404 and 403 follow 05 rule 2.
-
authz.view_as_max_durationis in the config schema;make config-doc-checkis green. -
make checkis green.
Definition of done¶
Code and tests as above; internal/authz/README.md updated (ports, operations, rules); doc comment
on every public symbol; no ADR needed unless you take an architecture decision beyond the rulings
listed (then write one); services/server/CHANGELOG.md entry under Unreleased; rulings in
docs/DECISIONS.md; make check green; an independent review and the red team
(docs/quality/red-team-gate.md) over the diff with the verdict recorded in the report; merge
locally into the base branch only if both are green, then git worktree remove.
Report¶
Write to {{REPORT_PATH}}: first line Verdict: DONE or Verdict: BLOCKED <reason>; then commits,
files with line counts, test results with counts (including the red runs), what is not proven,
follow-ups that could not be done in scope with reason. Then write {{DONE_PATH}} with
exit=0 sha=<sha> or exit=1 <reason>.