Skip to content

M1-48 Groups and grants over the API

No Date Ruling Reason
M1-48/1 2026-10-13 Base int/m1-wave12 at da08f58, branch feat/M1-48-authz-api in its own worktree; the branch is not merged locally and not pushed, the integration lane ports it. Text this ticket adds is English, also in German chapters and READMEs. Instructions of the launcher, which override the brief's merge step and language (as M1-44/1).
M1-48/2 2026-10-13 Non-owners see grants only inside their manage scope; filters narrow it and never widen it. The scope (spaces and channels with effective manage, a channel also through its space or the instance) comes from the rights snapshot per request (Authorizer.AdminScope) and is applied in the query. Brief ruling 1; 07 section 2.6 item 6.
M1-48/3 2026-10-13 A grant or group the caller may not read is 404 for every operation on it, with the same status, body and headers as a random id (AZ-P4). For grants this changes the visibility rule of M1-28: a grant on a space or channel is visible to owners and to holders of manage there, no longer to everyone who reads the target. Brief ruling 2 and the table "Visibility and authorization per operation"; reading a target does not make its administration data readable.
M1-48/4 2026-10-13 Revoking an already revoked grant succeeds with the stored grant, without change, event or rights version, and is audited with no_change (06 section 2 "No effect"). Every revocation needs a reason that is not blank after trimming; 07 section 9.5 says so. Brief ruling 3; 03 R-DM-09 and 07 9.5.
M1-48/5 2026-10-13 Deleting a group revokes its live grants with the reason group_deleted (revoked by the caller) in one rights change and keeps the memberships as history; Admin.DeleteGroup takes no reason. Brief ruling 4; deleteGroup has no body.
M1-48/6 2026-10-13 member_count and the member list show memberships in effect at the request: not removed, not expired (at expires_at already over). In group.changed of a deletion the count is that of the memberships kept as history. Brief ruling 5; 07 section 2.5.
M1-48/7 2026-10-13 The delegation limits (CheckCreate, CheckRevoke) run in the requirement step of domain.Decide (decideDelegation), after the requirement and before "view as" and step-up; the guard step keeps agent.invoke and token.create. Brief scope 3 and 07 4.2 "Requirement before step-up": a caller beyond the limits must not be asked for a step-up.
M1-48/8 2026-10-13 getGrant and the instance filter of listGrants check an instance grant as grant.read on the caller as resource principal, whose requirement is owner; for getGrant a forbidden becomes 404. A host grant is 404 until M2-39. listGrants without a target filter authorizes through AdminScope only: a non-owner without manage gets an empty page, not 403. 05 lists grant.read on space, channel, host and principal only; the brief makes instance grants owner only and lets a principal filter narrow instead of refuse.
M1-48/9 2026-10-13 createGroup with source = oidc_claim checks that provider_id names an existing provider through the foreign key groups_provider_id_fkey (400, /provider_id, invalid), not its kind. A local group with provider_id or external_ref is 400 with code forbidden; a taken claim is 409 conflict on /external_ref. authz must not read identity tables and has no port for providers; the claim sync of M1-42 only reads groups of OIDC providers, so a group bound to another kind stays empty and harmless.
M1-48/10 2026-10-13 updateGroup without name and description is 400 validation_failed on /name with code required; names are trimmed on create and update. The brief requires at least one of them; the schema GroupUpdate cannot say so.
M1-48/11 2026-10-13 Field codes of the new checks: in_past (an expiry that is not after the request; before not_after_creation), unknown_subject, unknown_user, duplicate (a capability listed twice), required and forbidden (a missing or forbidden id of a pair, in the body or as #query/<name> in listGrants). The audit details of grant.create name a group subject as subject_id, like every other subject. The brief's codes and audit details.
M1-48/12 2026-10-13 Files outside the brief's scope that the ticket had to change: internal/wire (construction, Handlers.Access, the configuration keys group.name_max_chars and group.description_max_chars with their tests), api/routes/real_test.go (the enabled table), cmd/bothy/config_test.go, tools/configdoc/planned.yaml (the rule group. is stale once the keys are registered) and the generated configuration reference. Enabling an operation needs its handler in the start (api/README "Enable an operation (handler ticket)"); a registered key must leave the planned list.
M1-48/13 2026-10-13 Red team round 1 (NEIN, two Majors), fixed with a test each: (1) createGroup and createGrant check the shape before authorize without the caller's id (domain.CheckGrantShape, CheckClaim, groupShape) and build the row after it, so an anonymous call is 401, not 500 (TestAnonymousWritesAre401); (2) behind the Idempotency-Key stage a raced revocation, deletion or removal records the call without effect inside the late rights change instead of returning errRaced, because a failed late rights change fails the whole unit (M1-31/2); that attempt raises permission_version without a difference, as M1-31/9 accepts for enableUser (TestConcurrentKeyedRevocations). Minors fixed: every operation with an id runs authorize before it answers a missing resource, the class of M1-31/16 (TestNoExistenceOracleBeforeVisibility); the reason of a revocation is checked like the reason of a grant (domain.CheckReason); deleting a group revokes only grants in effect, an expired one is reported as expired by the fold of the same change. Not changed: the sentence of 07 9.5 stays English (instruction of the launcher: everything English, M1-48/1); listGroups under "view as" and the receive action grant.read of grant.changed for resource grant have no caller before M1-49 and M1-65; until M1-52 and M1-53 wire a structure source, targeted grant reads of spaces and channels answer 404 in production, as every structure lookup does; AdminScope reads all spaces and channels per list request (bounded by the size of the instance, like Scope). Findings of the red team in the scope of this ticket.
M1-48/14 2026-10-15 Port onto main: code and tests are taken byte for byte from the integration squash, with the test world of INT-W14/7 without the identity keys of M1-45, which comes after this ticket on main. Code comments, the chapter keys of group.name_max_chars and group.description_max_chars and the rulings name the English chapter files and headings (03-data-model.md "Configuration values of this chapter", 06 "No effect", 07 4.2 "Requirement before step-up", api/README "Enable an operation (handler ticket)"); the sentence of 07 section 9.5 replaces the English sentence at the same place. The module README and the server changelog name the reasons step_up_not_possible and claim_managed_group after their registered codes (403 forbidden, 409 conflict), as the problem code check of main requires. The main-only case TestVisibleByResourceKind of M1-28 expects the visibility rule of M1-48/3: a grant on a space is hidden from a reader without manage and shown to a holder of manage. Generated files are regenerated, not merged. main is English and does not hold M1-45 yet; the case was added on main after the integration base and still asserted the rule M1-48/3 replaces. No other behaviour changes.