172 lines
12 KiB
Markdown
172 lines
12 KiB
Markdown
# Users & Accounts redesign — identity, honestly
|
|
|
|
Approved mock: `home-mocks/users.html` (scratchpad, :8642). Spec wins over mock on conflict.
|
|
|
|
## Goals
|
|
|
|
1. **Users**: profile hero (stock avatar gallery + file + remove), password change with strength
|
|
meter, live-validating add-user form, keep-files deletion choice, per-user management
|
|
(type / reset password / unlock), auto-login with the tradeoff stated.
|
|
2. **Fingerprint**: native guided enrollment (fprintd D-Bus), enrolled-finger list + removal,
|
|
and the stuck state made reachable — the card renders when the authselect feature is on
|
|
even with no reader, offering Turn off.
|
|
3. **Online Accounts**: "This desktop" cards (Home Assistant, phone) leading; GOA account cards
|
|
with confirmed removal and busy states; native add for password providers (Nextcloud, IMAP);
|
|
OAuth remains the single honest handoff. Page-level availability stops being gated on
|
|
transient write errors.
|
|
|
|
Non-goals: camera avatar capture, OAuth without GOA's dialog (impossible — no GIR), parental
|
|
controls, group management beyond account type, username changes.
|
|
|
|
## Helper extensions (pinned — as built)
|
|
|
|
**`scripts/panama-users`** (Python, unchanged language). Verbs:
|
|
`snapshot` · `stock-avatars` · `set-real-name USER NAME` · `set-icon USER PATH [X Y SIZE]` ·
|
|
`set-account-type USER standard|administrator` · `set-automatic-login USER true|false` ·
|
|
`set-locked USER true|false` · `set-password USER` (stdin) · `reset-password USER` ·
|
|
`create-user USERNAME REALNAME TYPE` · `delete-user USERNAME keep|remove`.
|
|
|
|
- `set-icon USER ""` clears the avatar (`SetIconFile("")`) — no separate verb.
|
|
- `stock-avatars` prints its own shape: `{"avatars":[{"name","path"}],"error":""}`. Absolute
|
|
paths, sorted by name; a missing `/usr/share/pixmaps/faces` yields `[]`. Fedora ships 15.
|
|
- `delete-user` takes `keep|remove`; the older `keep-files|remove-files` spelling still works.
|
|
- `reset-password USER` → `SetPasswordMode(1)`. No password material anywhere in the path.
|
|
- `set-account-type` now carries the last-admin refusal **in the helper** (same sentence as
|
|
`delete-user`), so a page that forgets the guard cannot demote the only administrator.
|
|
- Every mutating verb still answers with the fresh snapshot plus `error`, exit 0.
|
|
|
|
**`scripts/panama-fingerprint`** — **converted bash → Python** (a D-Bus signal loop is not a
|
|
thing bash can do). `status` and `set-unlock` keep their exact CLI and JSON, and still shell
|
|
out to `fprintd-list` / `authselect` / `panama-sudo --reason`, so the existing PATH-stub seam
|
|
is untouched. Verbs: `status` · `set-unlock on|off` · `enroll FINGER` · `remove FINGER` ·
|
|
`remove-all`.
|
|
|
|
- `status` → `{reader, readerName, enrolled, pamEnabled, unlockFeatureEnabled, error}`.
|
|
`pamEnabled` and `unlockFeatureEnabled` are the same fact under the old and new name.
|
|
**Behaviour change for C**: authselect is now parsed unconditionally, so a readerless
|
|
machine reports `pamEnabled: true` when the feature is on. That used to be forced to
|
|
`false`, which is what made the stuck state unreachable. (This machine is in it.)
|
|
- `enroll FINGER` streams one JSON object per line:
|
|
`{ok, stage, done, total, result, error}` with `stage ∈ claiming|scanning|done|failed`.
|
|
fprintd `Claim` → `EnrollStart` → one line per `EnrollStatus` → `EnrollStop` → `Release`.
|
|
Cancellation is SIGTERM (stopping the Process); the helper still releases the device.
|
|
Exit 0 on `enroll-completed`, 1 otherwise. 90s idle watchdog.
|
|
- `remove FINGER` / `remove-all` → `DeleteEnrolledFinger` / `DeleteEnrolledFingers2` under a
|
|
Claim, and answer with the **`status` shape** so the service has one absorb path.
|
|
- Finger vocabulary lives in `services/Fingerprint.qml` (`allFingers`, `fingerLabel`); the
|
|
helper only shape-checks the name.
|
|
- Fixture seam: `PANAMA_FINGERPRINT_FIXTURE` = JSON file
|
|
`{"enrollStages":N,"results":["enroll-stage-passed",…],"error":""}`; every D-Bus call it
|
|
stands in for is appended to `PANAMA_FINGERPRINT_LOG` (default `<fixture>.log`) as
|
|
`{"method","arguments"}`. No bus, no gi, no reader.
|
|
|
|
**`scripts/panama-accounts`**: `list` **and `snapshot`** (alias; the service asks for
|
|
`snapshot`) · `set` · `remove` · `add-nextcloud SERVER USERNAME` ·
|
|
`add-imap EMAIL IMAP_HOST SMTP_HOST USERNAME`. Passwords on stdin, never argv. Both adds
|
|
answer with `{"accounts":[…],"error":""}`.
|
|
|
|
- **GOA's add flow, verified**: `Goa.Manager.AddAccount(provider, identity,
|
|
presentation_identity, credentials a{sv}, details a{ss}) → o` is fully daemon-side —
|
|
goa-daemon writes `accounts.conf` and calls `goa_utils_store_credentials_for_id_sync`
|
|
itself. The missing libgoa-backend GIR blocks only the *client-side sign-in dialog*, so
|
|
password providers are genuinely addable and OAuth genuinely is not. `IsSupportedProvider`
|
|
is checked first (`owncloud`, `imap_smtp` both true here; `nextcloud` is not a provider
|
|
type — `owncloud` is).
|
|
- Keys are GOA 3.58's own, taken from `goaowncloudprovider.c` / `goaimapsmtpprovider.c` and
|
|
confirmed against this machine's `accounts.conf`. Nextcloud: credentials `{password}`,
|
|
details `Uri` (`…/remote.php/webdav/`), `FilesEnabled`, `CalendarEnabled`, `CalDavUri`
|
|
(`…/remote.php/dav/`), `ContactsEnabled`, `CardDavUri`, `AcceptSslErrors`. IMAP:
|
|
credentials `{imap-password, smtp-password}`, details `Enabled`, `EmailAddress`, `Name`,
|
|
`Imap{Host,UserName,UseSsl,UseTls,AcceptSslErrors}`,
|
|
`Smtp{Host,UseAuth,UserName,AuthLogin,AuthPlain,UseSsl,UseTls,AcceptSslErrors}` — IMAPS +
|
|
submission/STARTTLS defaults, one password for both servers.
|
|
- The daemon validates nothing, so each add calls `EnsureCredentials` on the new object and
|
|
**removes the account again** if the credentials are refused, reporting why.
|
|
- Fixture seam: `PANAMA_ACCOUNTS_FIXTURE` = a JSON file in `list`'s shape. With it set nothing
|
|
imports gi; validation still runs; each verb appends `{"verb","arguments","passwordBytes"}`
|
|
to `PANAMA_ACCOUNTS_LOG` (default `<fixture>.log`) — the password's **length**, never itself.
|
|
|
|
## Services (A — as built)
|
|
|
|
- **UserAccounts**: `removeIcon()` (no argument — the hero card's own avatar),
|
|
`stockAvatars` + `loadStockAvatars()` (cached; call it when the gallery opens),
|
|
`deleteUser(user, keepFiles)` — **the bool inverted from the old `removeFiles`** —
|
|
`resetPassword(user)`, `setAccountTypeFor(user, type)` (**renamed** from `setAccountType`),
|
|
`setLocked(user, locked)`. `locked` and `loginTime` stay in the payload for the UI.
|
|
- **Fingerprint**: `readerPresent`, `readerName`, `unlockFeatureEnabled`, `fingers`
|
|
(**renamed** from `enrolled`), `allFingers`, `availableFingers`, `fingerLabel(f)`,
|
|
`cardVisible` (= `readerPresent || unlockFeatureEnabled`), `busy`, `lastError`,
|
|
`enrolling`, `enrollPhase`, `enrollFinger`, `enrollStage`, `enrollTotal`, `enrollResult`,
|
|
`refresh()`, `setUnlockEnabled(on)`, `startEnroll(finger)`, `cancelEnroll()`,
|
|
`removeFinger(f)`, `removeAll()`.
|
|
- **OnlineAccounts**: `available` (GOA answered the last snapshot — persisted, never touched
|
|
by a write), `snapshotError` / `writeError` and the derived `lastError`, `busy` (derived
|
|
from the Processes), `attentionCount`, `refresh()` (cheap; call on every page open),
|
|
`setService`, `remove`, `addNextcloud(server, user, password)`,
|
|
`addImap(email, imapHost, smtpHost, user, password)` — password via Process stdin.
|
|
|
|
## UI (B)
|
|
|
|
**UsersPage.qml** rebuilt per mock: hero card (avatar, Change picture… opens a popover with
|
|
stock gallery + "Choose a file…" → existing AvatarPicker/Cropper flow, Remove button);
|
|
Account card (Full name — live-committing field, Username honest row, Account type segment
|
|
with the last-admin reason as detail, Password flow with a 4-segment strength meter — local
|
|
heuristic, length + classes, no network; Automatic login with the mock's tradeoff copy);
|
|
Fingerprint card per mock (stuck row with NO READER badge + Turn off when
|
|
`unlockFeatureEnabled && !readerPresent`; full experience otherwise: toggle, enrolled list
|
|
with per-finger Remove…, Add a fingerprint → inline enrollment panel driven by the enroll
|
|
stream — touch icon, stage counter "N of M touches", cancel; no continuous animation);
|
|
Other accounts card (expandable per-user rows: type segment, Reset password ("They set a new
|
|
one at next sign-in"), Delete with a keep-files dropdown [Keep the files / Remove everything]
|
|
+ two-stage confirm; locked users show Unlock; add-user form with LIVE validation — use live
|
|
TextFields, not blur-committing TextFieldRow, with the username rules + 31-char cap in the
|
|
detail and mirrored in the enabled predicate).
|
|
|
|
**OnlineAccountsPage.qml** rebuilt: gains `objectName: "accounts"`; "This desktop" card —
|
|
Home Assistant row (state from the HomeAssistant service: connected/rooms, else "not set up",
|
|
button → `openSettings("my-home")`) and Phone row (KdeConnect state, → `openSettings("phone")`);
|
|
Accounts card — one expandable row per GOA account (per-service toggles disabled while
|
|
`busy`, Remove with two-stage confirm), needs-attention rows keep "Sign in again"; Add card —
|
|
Nextcloud + Mail native forms (server/user/password, password field cleared on success or
|
|
close), and the OAuth row with the mock's honest copy → the existing GNOME dialog handoff
|
|
(the ALLOWED exception stays for accounts). Errors render as a row, never as page-wide
|
|
unavailability.
|
|
|
|
## Search & docs (C)
|
|
|
|
New entries: Fingerprint, Enroll a fingerprint, Delete a user, Account type, Reset a password
|
|
→ users; Nextcloud, Google account, Remove an account, Add a mail account → accounts. Docs
|
|
regen only if schema changes (none expected — verify).
|
|
|
|
## Contracts (C — write; hermetic stub runs only)
|
|
|
|
- `user-accounts-contract`: extend — keep-files arg end to end, reset-password sends no
|
|
password material (SetPasswordMode pinned), remove-icon empty-string call, stock list shape,
|
|
live-validation predicate present in the page (the blur-commit bug regression pin: the add
|
|
form's fields must not be TextFieldRow).
|
|
- `fingerprint-contract`: major rework — enrollment is native now (streamed stages against a
|
|
stubbed fprintd D-Bus layer or a stub helper — follow the hermetic pattern), the GNOME
|
|
enrollment handoff needle INVERTS (page must NOT call openGnomePanel for users), stuck-state
|
|
visibility pinned (`readerPresent || unlockFeatureEnabled`), finger vocabulary single-source
|
|
kept, set-unlock reason kept.
|
|
- `gnome-handoff-contract`: delete the `UsersPage.qml:system-users` ALLOWED exception; the
|
|
`OnlineAccountsPage.qml:online-accounts` exception stays with its reason updated to name
|
|
OAuth specifically.
|
|
- NEW `online-accounts-contract`: hermetic — availability decoupled from write errors,
|
|
passwords via stdin never argv, add flows validate inputs, remove requires the confirm
|
|
state, snapshot exposes no secrets.
|
|
- Backlog Phase 11; README count line (171 → 172 expected).
|
|
|
|
## Agent ownership (parallel)
|
|
|
|
- **A**: `scripts/panama-users`, `scripts/panama-fingerprint`, `scripts/panama-accounts`,
|
|
`services/UserAccounts.qml`, `services/Fingerprint.qml`, `services/OnlineAccounts.qml`.
|
|
- **B**: `modules/settings/UsersPage.qml`, `OnlineAccountsPage.qml`, new components
|
|
(+ qmldir); may read AvatarPicker/AvatarCropper and reuse unchanged.
|
|
- **C**: `services/SettingsSearch.qml`, contracts above, backlog, README count line.
|
|
|
|
Hard rules: NO live mutations — no user/password/icon/type changes, no GOA account writes,
|
|
no authselect, no fprintd enrollment against real hardware (none exists here anyway), no
|
|
polkit-triggering calls. Read-only probes and stubs only. Valid QML/Python at every save.
|
|
B programs against the pinned APIs; A updates this spec before changing them.
|