Skip to content

M1-31 People, owners and own profile

No Date Ruling Reason
M1-31/1 2026-10-12 Built on feat/M1-31-identity-people from int/m1-wave7 (8e30539) in its own worktree; not merged locally, not pushed; the PR lane ports it. Instructions of the launcher, which override the brief's local merge and git worktree remove.
M1-31/2 2026-10-12 Resolves M1-20/4: the Idempotency-Key stage opens its unit with txn.AllowLateRightsChange(). The first nested Do that declares a rights change in such a unit runs the rights begin hook, its function and the rights check hook in this order inside the ambient transaction (txn.UnitControl.Declare, platform/pg lateRightsChange); sync and audit run at the end of the outer unit; a second rights change in the same unit is an internal error. The stage has written only its key row when the use case starts, so permission_version is still locked before any domain row (03 "Lock order"), and only the use case knows the affected principals. Running the check at the end of the nested unit hands i1_violation and last_owner to the handler, so the stage stores them like any final 409.
M1-31/3 2026-10-12 The actor-based use cases are the ports identity.People and identity.Owners with the DTOs of the brief (UserView and OwnerView serve as user and owner entry); identity.Directory stays the actor-free read port of other modules. The brief adds the methods to Directory, whose contract (M1-26) is "does not call authorize"; two meanings in one interface would trap the modules that use it. Only the names differ.
M1-31/4 2026-10-12 listUsers and listOwners authorize user.read on the resource kind user without id and with OwnerID = the caller, not on the instance. The catalogue gives user.read only the kind user (authorize fails closed with an internal error for instance). The descriptor owned by the caller is visible to every caller (M1-28/D4), so the requirement "human; apps with scope" decides: people and apps read the directory, an agent gets 403 forbidden (subject_kind_not_allowed) as test 11 asks.
M1-31/5 2026-10-12 An owner with a personal token gets 403 forbidden with reason: owner_only for the five owner writes, not step_up_not_possible as the brief's test 2 states. Same as M1-29/18: the requirement step decides before the step-up step, and with a token the owner role does not take effect (SR5). The rights logic stays in authz.
M1-31/6 2026-10-12 updateMyProfile by an agent or app is 403 forbidden with reason: subject_kind_not_allowed, decided by identity after authorize allowed self.profile.update. The requirement of self.profile.update is self, which an agent naming itself meets; the profile is a row of users, which only people have (03 users), so the use case refuses the kind like a missing resource of the wrong kind, with the reason code of 07 section 4.1.
M1-31/7 2026-10-12 createUser emits user.updated; addOwner for a person that is not active is 409 conflict (reason: principal_not_active), for an owner 409 conflict (reason: already_owner); enableUser on an active person is an audited call without effect (200, no_change: true, no event, no rights version); revokeOwner without an active owners row is 404. Rulings of the brief; the reasons name the cause (06 section 5 point 3).
M1-31/8 2026-10-12 local_account: true on createUser is 400 validation_failed with pointer /local_account and code not_enabled until M1-37 adds enrollment codes; M1-37 replaces this rule. Interim rule of the brief; not_enabled is the field code of 06 section 5 point 3 for a function this server version does not build yet.
M1-31/9 2026-10-12 addOwner, revokeOwner and enableUser authorize once per request in a first unit without rights change (also loading the resource and checking reason and state), then write in a second unit declared as the rights change, which locks the person (the owner changes also the active owners rows, in ascending id) and checks the state again but not the authorization. Every rights change is serialized by the lock on permission_version of the rights begin hook, so I1 and SR8 hold for enableUser without the owners lock. Two owners revoking each other at once give one 204 and one 409 last_owner. A raced enableUser that finds the person already active answers as without effect (no event); inside the Idempotency-Key stage the rights version of that attempt may have been raised. A request is judged by the rights at its start (R-RE-02, one rights version per request); SR8 and I1 guard the result under the lock (test 5 of the brief). A refused call never raises the rights version.
M1-31/10 2026-10-12 q of listUsers uses starts_with(lower(column), lower(q)) instead of LIKE with escaped % and _. The same literal prefix match without an escape rule to get wrong; held by test 11.
M1-31/11 2026-10-12 Locales are parsed by adapters/locale (golang.org/x/text/language, vendor group xtext, now a direct dependency of go.mod) behind the port app.Locales; the domain imports time/tzdata for time.LoadLocation on hosts without zoneinfo. A time zone must also have the canonical form of an IANA name (first a pattern; since red team round 2 the generated list zoneNames of tools/zonegen, see M1-31/17), because the server image does carry a system zoneinfo directory whose other files (posixrules, right/..., posix/...) and spellings (Europe//Berlin, ./UTC, lower case on a case-insensitive file system) time.LoadLocation accepts too. The module register grants vendors to adapters only (go-arch-lint depOnAnyVendor: false); x/text is BSD-3-Clause and was already in go.sum. Red team round 1, Major 4.
M1-31/12 2026-10-12 getMe reads the rights version through instance.Versions, pins it with authorize.WithPermissionVersion and then authorizes self.read, so permission_version is exactly the version of the check. The version of the Authorizer's request memo is private to authz; pinning needs no new port.
M1-31/13 2026-10-12 Responses are validated by httpservertest.ResponseValidator over github.com/pb33f/libopenapi-validator (already the request validator's library, JSON Schema 2020-12 through santhosh-tekuri/jsonschema/v6 inside it) and the harness option ValidateResponses, not by a new test/apitest package with a direct santhosh-tekuri/jsonschema dependency. The test authenticator httpservertest.Actors with InSession and WithToken builds the principals of the tests. The brief allows "the response validator of the merged tree"; reusing the library of the request side adds no dependency and no component outside the module register.
M1-31/14 2026-10-12 A handle with upper case is 400 (pattern) before uniqueness is checked, so "duplicate handle in other case" of test 8 is a 400; a duplicate handle in the same case and a duplicate e-mail in any case are 409 conflict (/handle, /email). The check and the API pattern allow lower case only (M1-26); the unique index on lower(email) covers e-mail.
M1-31/15 2026-10-12 The schema User has no e-mail member, so getUser, listUsers, the owner list and user.updated never carry an e-mail address; only getMe reports the caller's own. audit.reason_max_chars (19 SEC-AUD-5) is not enforced on the reasons of enableUser, addOwner and revokeOwner: the key is not registered yet and the brief adds no key; the body limit of the chain bounds them. Contract of docs/api/openapi.yaml; the length rule belongs to the ticket that registers the key.
M1-31/16 2026-10-12 Red team round 1 (NEIN, four Majors), fixed with a test each: (1) GetUser, UpdateUser, EnableUser, AddOwner, RevokeOwner and the device reads getDevice and listUserDevices of M1-33 call authorize before they answer a missing person, owner role or device with 404, so an anonymous caller, a pending device or a person without owner role gets the same answer for an existing and an unknown id (checkThenFound; the second occurrence of the class, fixed for both tickets); (2) q of listUsers must be valid UTF-8 without control or bidi characters (400, #query/q), a cursor whose handle is no valid handle is cursor_invalid, and SQLSTATE 22021 maps to 400; (3) a locale longer than 35 characters (RFC 5646 section 4.4.1) is 400 validation_failed with the field code too_long before it is parsed; (4) the strict zone form of M1-31/11. Minors fixed: a failed late rights change fails the whole unit even when its caller swallows the error, and a nested unit the late declaration covers may join while it runs; an empty locale or time_zone on createUser is 400 like on updateUser (CreateUserCmd takes pointers); the privacy test captures the access log of slog.Default; the response validator test has the undeclared status. Not changed: REST times are truncated to milliseconds but encoded by encoding/json without trailing zeros (also in M1-33); 06 section 2 lets clients read every RFC 3339 form, and the encoding belongs to the generated layer of all operations, not to this ticket. Rules of 07 section 4.2 and 02 section 9.2 (no existence oracle), SEC-T06 (no unexpected 5xx), SEC-IN-4.
M1-31/17 2026-10-12 Red team round 2 (NEIN: two Blockers, two Majors), fixed with a test each: (B1) the handler tests build one response validator per process, contract and operation list, over the contract pruned to the mounted operations like the request validator (httpservertest.SharedResponseValidator, validation serialized; one build under -race went from about 72 s to about 6 s and the package from 275 s to 55 s in this run), so the HTTP test package of identity stays far below the 10 minute limit of go test -race; (B2) the use cases, the people queries, httpservertest and kernel/txn.UnitControl have tests in their own packages (identity/app/people_test.go, adapters/postgres/people_test.go, httpservertest_test.go, txn/late_test.go), so the coverage of new code does not depend on tests of other packages; (M1) a cursor handle is checked against the handle pattern only, not principal.handle_max_chars, so a handle created under a larger limit still pages; (M2) a time zone must be one of the IANA names of the toolchain's tzdata, generated into domain/zones.gen.go by tools/zonegen (without Factory, which ICU refuses), which makes the rule the same on case-insensitive and case-sensitive hosts. Minors fixed: UpdateUser authorizes before it locks the row; a recovered panic in a late rights change fails the unit; the locale bound counts characters and its value is pinned by a test; the docs of the device reads, the owners lock of enableUser and the CheckSearchPrefix comment. M1-33/2 ("an unknown person is not_found before authorize") is superseded by M1-31/16 for listUserDevices. Not changed: a raced enableUser and the swallowed errRaced of authz (M1-48 must hand it on behind the stage, noted at txn.AllowLateRightsChange); the reason length of SEC-AUD-5 (M1-31/15); unchecked rights writes after a late rights change in the same unit (no caller; the rule is in the godoc). Same rules as round 1; the test-time limit and Sonar's new-code coverage are gates of CI.
M1-31/18 2026-10-13 The test binary of internal/wire builds the request validator of the enabled operations in TestMain (WarmEnabledValidator, export_test.go) before any test starts. Second occurrence of the class of M1-33/18: with thirteen enabled operations the one build per process grew, and under -race and a host load around 25 the first serve tests (TestServeTLSPublicListener, TestConsumerLagGaugesInApiRole, TestStart_*) waited on it inside their 30 s readiness wait and timed out in make check, while they pass alone. Building it before the tests removes the build from every per-test deadline, whatever later tickets enable.
M1-31/19 2026-10-13 Port onto main: the paragraph on rights changes behind the Idempotency-Key stage stands in 02 section 8.4 of the English chapter, and the chapter references name the English files and section titles (03 "Lock order"); M1-31/16 names a too long locale as 400 validation_failed with the field code too_long. The specification is English on main, and the problem code gate accepts only codes of 06 section 5; no behaviour changes.
M1-31/20 2026-10-15 Port onto main: tools/zonegen takes the default zip from $GOROOT, which go generate sets for the generator, and is a usage error (exit 2) without -zip and GOROOT; it no longer starts go env GOROOT (TestDefaultZipFromGOROOT). SonarQube Cloud rated the command started through PATH as a vulnerability on new code; the generator needs no command of its own.