M4-26: Administration: people, groups and devices¶
Status: ready | Depends on: M4-25 | Milestone: M4
Goal¶
After this ticket the administration of 01 5.2 exists in packages/admin_ui with its shell (the navigation of A1: People, Groups, Grants, Devices, Agents, Apps, Audit, Statistics, Instance, Hosts, where the later tickets fill their entries) and the screens for people, groups and devices: the people list with search and filters, creating a person (no self-registration, no invitation links; an enrollment code shown once), a person's detail (owner flag, credentials, identities, passkeys, devices, sessions; disable and enable; new enrollment code for recovery), owners (add and revoke with the last-owner rule), groups (create, rename, delete, members with optional expiry), and the queue of waiting devices with the origin of the login (approve with the verification code, reject). Every changing action shows a confirmation and then the step-up of the step-up device key exactly where 05 marks the action with step-up (docs/spec/actions.yaml), through the step-up dialog of M4-25. The desktop app opens the shell at /admin. Why: administration is where permissions change; a screen that skips the step-up or invents its own permission logic would undermine SR5 and principle 1 of 15 (B7).
Read first¶
docs/PLAN.md: section 3; section 10 rows M4-25 to M4-29, M4-37; section 12 rows M6-04, M6-07 (the console reuses these screens).docs/spec/01-product.md: 3.7 "Administration, operations, integration", 5.2 "Administration" (the rule above A1 and A1 "People"), 6.7 (destructive actions confirm once; step-up asks the device), 13 (R-PR-07).docs/spec/08-identity.md: 3 (enrollment codes: shown once,en_format, purposes), 4 (owners, last owner), 5.2 (approval by an owner with the verification code), 5.4, 10, 11 and 12 (disable and enable a person, what ends), 6 (step-up).docs/spec/07-permissions.md: 2.1 (subjects, groups), 2.7 (owner), 5 SR5 (administration only by humans with confirmation), SR7 (disabled means no permissions), SR8 (last owner), 9.5.docs/spec/05-actions.md2.4 (columns "Owner" and "Step-up") anddocs/spec/actions.yaml(thestep_upvalue of every action the screens call).docs/api/openapi.yaml:listUsers,createUser,getUser,updateUser,disableUser,enableUser,createEnrollmentCode(EnrollmentCode,one_time_secret),getUserCredentials,unlinkIdentity,revokePasskey,listUserDevices,listUserSessions(Sessionwithdevice_idandauth_method),listOwners,addOwner,revokeOwner,listGroups,createGroup,getGroup,updateGroup,deleteGroup,listGroupMembers,addGroupMember,removeGroupMember,approveDevice,revokeDevice,revokeSession,getMe(is_owner); problem codeslast_owner(SR8,409) andi1_violation(shown, resolved in M4-28).docs/DECISIONS.md: D-W-100, D-W-114; section 10.- Briefs:
docs/briefs/M4-25-settings-devices.md(admin_uilayout,SelfRepositorypattern,StepUpDialog),docs/briefs/M4-09-auth-dpop.md(StepUpInterceptor),docs/briefs/M4-07-core-components.md,docs/briefs/M4-17-fake-server.md(handler registry); for the server sidedocs/briefs/M1-31-identity-people.md,docs/briefs/M1-37-identity-bootstrap.md,docs/briefs/M1-45-identity-revocation.md,docs/briefs/M1-48-authz-api.md.
This ticket touches no API operation, no table of chapter 03, no action, no event and no configuration key.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- 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.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them, and the ARB test of M4-06 rejects them in interface texts. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. Interface texts come frompackages/l10n, never as literals in widgets. - No permission logic: a section, a button or a field appears according to the server's answers (
getMe.is_owner, a403or404of the list call, the capabilities a response names), never from an own reading of grants (15 1). - The step-up is the server's decision (
401 step_up_required); the screen never skips it and never adds one the server does not ask for.
Product requirements implemented here:
- Every changing administration action with step-up shows a confirmation and then the step-up with the device key, before the change is submitted (01 5.2, SR5).
- Creating a person gives an enrollment code that is shown once, copyable, read character by character; a repeated request does not show it again (
409withone_time_secret) (08 3). - Disabling a person ends their sessions and shows that in the confirmation; enabling may hit I1 (
409 i1_violationexplained, resolved by M4-28) (08 11, 10 5.4). - The last owner cannot be revoked; the screen shows the server's reason (SR8).
- The device queue shows platform, name, time and origin of the login. The API has no instance-wide list of waiting devices and no origin field on
Device; the queue is therefore built fromlistUsers,listUserDevicesfiltered tostate = pending, and the origin fromauth_methodof the session of that device inlistUserSessions(Sessioncarriesdevice_idandauth_method); the shell's count comes from the same data. TheDeviceschema has no hardware or attestation field either; 01 A1 shows "Key: hardware, attested", so the queue leaves that line out. The report names both gaps with the recommendation of a list operation for waiting devices (03devicesalready names its index); approving needs the verification code from the waiting device; rejecting revokes it (08 5.2). - Group members can have an expiry; removing and adding members, renaming and deleting groups (07 2.1).
- The administration is a package shared with the console; it uses REST only (15 3.1).
Scope¶
Create in packages/admin_ui/lib/src/:
shell/admin_shell.dart(navigation of A1 with the entries of 01 5.2; an entry without a screen yet shows nothing and is not listed; the "waiting devices" count),shell/admin_routes.dart(route names the apps map to/admin/...).people/(people_list.dart,person_detail.dart,create_person_dialog.dart,enrollment_code_view.dart,credentials_section.dart,owners_section.dart),groups/(groups_list.dart,group_detail.dart,member_editor.dartwith expiry),devices/(waiting_devices.dart,approve_device_dialog.dart),data/admin_repository.dart(REST),common/confirm_change.dart(one confirmation with what changes, then the call that the step-up interceptor completes),common/problem_banner.dart(problem codes throughproblemText).- Tests under
packages/admin_ui/test/admin/and goldens (Tests). apps/client/lib/admin/admin_route.darthosting the shell at/admin.- Fake server handlers through the registry of M4-17 for the operations above, demanding step-up for exactly the actions whose
step_upinactions.yamlistrue(Design note 3). - ARB keys for every new text in
packages/l10n.
Change:
packages/admin_ui/README.md(shell, screens, how the console embeds them),apps/client/README.md.docs/DECISIONS.mdsection 10: rowsD-M4-26-<n>; rootCHANGELOG.mdunder[Unreleased].
Out of scope¶
- Grants, "Who has access?" and "View as person" (M4-27); agents, backend switch and costs (M4-28); audit (M4-29); hosts (M4-37).
- Apps, statistics and instance settings (M6-08, M6-06, M6-09).
- Identity providers and login methods of the instance (M6-09).
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M4-26-admin-people; one writer per worktree. - Conventional Commits with the package or component as scope (for example
feat(client): ...,feat(sync-engine): ...,build(gates): ...,docs(spec): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. Never publish a Dart package (publish_to: nonein everypubspec.yaml). - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests bind only
127.0.0.1with port 0 and make no network call; fakes replace the server unless a section below names a container. Temporary directories only viamktemp -d(DartDirectory.systemTemp.createTemp, pytesttmp_path), removed at the end. Stop only processes you started, by PID. - Never read or print secrets. No network calls except package downloads for pinned tools and pub packages and what the gates of
make checkdo themselves. - The Flutter SDK at the version pinned in the root
pubspec.yamlmust be onPATH(and Go and Docker where a section names them); without them the gates fail by design (R-QA-02), and the ticket stops withVerdict: BLOCKED <tool> missinginstead of skipping. - Never lower a threshold, disable a gate, delete a test, raise
repo.max_file_kbor extendquality/licenses/allowed.txt; never exclude hand-written code from a lint, format, coverage or doc gate. - The product name appears only at the places of 02 "Code layout" ("Places that carry the product name"); the app shows
branding.product_namefrom the instance, never a literal. - No permission logic in the client (15 1 principle 1): controls follow server responses, snapshots and events, never an own derivation from grants.
- If the core contract (chapters 00, 03, 04, 05) or
docs/api/lacks something this ticket needs, stop withVerdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M4 by D-W-114). Where the merged code of an earlier ticket names a package, type, function, file, provider or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Repository and screens.
AdminRepositorywraps the generated API with typed results andApiProblem; screens are widgets that take the repository and aStepUpServicethrough constructor parameters (the console passes its own, M6-04). Lists page with the API cursor; search and filters are request parameters where the API has them, otherwise local filters of the loaded page with a visible note. -
Confirmation then step-up.
ConfirmChangeshows one dialog with what the change does in plain words (for example "Sam is disabled. All sessions of Sam end now.") and "Confirm"; then the call runs through theStepUpInterceptorof M4-09, which opens theStepUpDialogof M4-25 when the server answers401 step_up_required, and repeats the call with the sameIdempotency-Key. Destructive actions (revoke, delete, disable, reject a device) use the danger style ofDsBannerand ask once (01 6.7). -
Step-up per 05 in tests. Widget tests in
packages/admin_uiuse an in-package fake ofAdminRepositoryand a fakeStepUpService, becauseadmin_uimay not importsync_engine/testing.dart(15 3.1, the arch check of M4-01). The step-up enumeration runs as an integration test of the desktop app,apps/client/integration_test/admin_step_up_test.dart, againstFakeServer: the fake server readsdocs/spec/actions.yaml(walk up to the repository root) and answers401 step_up_requiredfor an operation whosex-authz-actionhasstep_up: trueuntil the client completes the step-up. A test enumerates every operation the screens of this ticket call, looks up its action andstep_up, and asserts that the screen goes through the step-up dialog exactly for those (and never forfalse). This holds "every action requires step-up according to 05" without a copy of the table. -
Enrollment codes. Shown once in
CodeTextof M4-18's pattern (move it toadmin_uiordesign_systemif the app's version cannot be imported from a package; packages never import fromapps/, 15 3.1 rule 2), copyable, with the expiry; leaving the view discards it from memory. -
Rulings for docs/DECISIONS.md (
D-M4-26-<n>): repository and injected step-up (note 1); confirmation before the step-up (note 2); the step-up enumeration test (note 3); where the code view lives (note 4).
Tests (write them first)¶
Widget tests in packages/admin_ui/test/admin/ (in-package fake AdminRepository, fake StepUpService):
people_test.dart: list, search, create with a one-time code view, disable with the sessions sentence, enable withi1_violationexplained, credentials and devices of a person, owners with the last-owner rejection.groups_test.dart: create, rename, delete, members with expiry.waiting_devices_test.dart: the queue is assembled fromlistUsers,listUserDevices(pending only) andlistUserSessions(origin fromauth_method); approve with the code; reject.apps/client/integration_test/admin_step_up_test.dart(Design note 3, againstFakeServer): every changing operation of these screens goes through the step-up dialog exactly whenactions.yamlsaysstep_up: true(andfresh;conditionalas 05 2.4 defines it).no_permission_logic_test.dart: a403on a list hides the section with the server's reason; no widget reads grants.- Goldens of the shell, people list, person detail, groups and the device queue; light and dark, two densities, text scaling 1 and 2,
deanden.
Commands that must pass, from the repository root:
cd packages/admin_ui && flutter analyze --fatal-infos && flutter test --coveragecd apps/client && flutter analyze --fatal-infos && flutter test --coverage- on macOS and on Windows:
make apps-platform(runs the integration test); elsewhere it prints its SKIP line make arch,make lint,make fmt-check,make coveragemake check
Acceptance criteria¶
- Widget tests of A1 are green; every changing action requires step-up exactly as 05 marks it.
- People, owners, groups with member expiry and the device queue work against the fake repository and, in the integration test, against the fake server, with the server's problem texts.
- Enrollment codes are shown once; no permission logic in the screens.
-
admin_uiuses REST only (make arch); rulings recorded. -
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why and cite 01, 05, 07 or 08.
packages/admin_ui/README.md(English) as in Scope.- Rulings of Design note 5 in
docs/DECISIONS.mdsection 10. - Root
CHANGELOG.mdentry under 'Unreleased'. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check. - Review and red team verdicts and the findings fixed.
- The list of operations with their action and
step_upvalue as the enumeration test saw them. - What is not proven (for example a platform test that ran only in CI, or a check done by hand).
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100, D-W-114), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.