Add the administration pages to the web interface
Running the server meant running the API by hand: invites, accounts,
groups and grants existed only as endpoints and CLI commands, so the
browser could create a vault and nothing else.
/web/groups walks the group tree and opens onto one group's members,
roles and vault access, including the grants it inherits from above and
where they come from. /web/users lists the accounts you may see, and
lets a server administrator disable, enable, clear a lost second
factor, promote, demote or delete one. /web/invites shows the
outstanding links with their use counts, issues new ones and revokes
them.
Every administrative change is now a function of the state and the
caller in admin.go, which both the JSON handlers and the forms call. The
alternative was a second copy of the permission rules behind the pages,
which would have drifted on the first change. The same move gave the API
the two verbs it was missing: DELETE /api/v1/invites/{token} and
PATCH /api/v1/users/{id}.
Account changes are server-administrator only. A group administrator
decides who reaches which vault; disabling an account or clearing
somebody's second factor reaches past the group they share. Disable and
delete revoke that account's sessions, and the three changes that would
lock the caller out are refused on their own account.
Pages go through webPage and forms through webForm, so the session and
CSRF checks cannot be forgotten on a new form. No page carries any
JavaScript.
Server 0.6.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3a499532f3
commit
81094c0228
+50
-12
@@ -29,7 +29,8 @@ cmd/keeporg/ desktop client: login, run, sync, status, administration
|
||||
internal/store/ domain model, JSON metadata store, ACL resolution
|
||||
internal/blob/ versioned KDBX payload storage on disk
|
||||
internal/kdbx/ KDBX 4 codec and the merge engine, with its own crypto
|
||||
internal/server/ HTTP surface: auth, OIDC, sync, admin, SSE hub, web, pages
|
||||
internal/server/ HTTP surface: auth, OIDC, sync, admin, SSE hub, web,
|
||||
web administration, page templates
|
||||
internal/client/ shared client engine: API wrapper, merge policy, mirror
|
||||
internal/secret/ password verifiers, tokens, identifiers
|
||||
internal/totp/ RFC 6238 second factor
|
||||
@@ -343,18 +344,54 @@ expected request sequence.
|
||||
|
||||
## The web interface
|
||||
|
||||
The server has one browsable page set, at `/web`, and it exists for the one
|
||||
job a command line does badly: creating a vault and setting its password.
|
||||
Sign in, see which vaults you can reach, create one. It never shows a vault's
|
||||
contents — the server opens a vault only to merge it — and there is no
|
||||
entry browsing, by design.
|
||||
`/web` is the browsable half of the server. It covers what a command line
|
||||
does badly — creating a vault and setting its password, and the daily
|
||||
administration of people — and nothing else. It never shows a vault's
|
||||
contents: the server opens a vault only to merge it, and there is no entry
|
||||
browsing, by design.
|
||||
|
||||
| Page | What it does |
|
||||
| --- | --- |
|
||||
| `/web/vaults` | the vaults you can reach, the create-a-vault form, your own account password |
|
||||
| `/web/groups` | the group tree you can see, and a form to add a group inside one you administer |
|
||||
| `/web/groups/{id}` | one group: members and their roles, the vaults it reaches and where that access comes from, grants given and revoked, deletion |
|
||||
| `/web/users` | the accounts you may see; for a server administrator, disable, enable, clear the second factor, promote, demote, delete |
|
||||
| `/web/invites` | outstanding invite links with their use counts and expiry, a form to make one, a button to revoke one |
|
||||
|
||||
The navigation offers the last three only to somebody who administers a
|
||||
group; a plain member sees their vaults and nothing else. The group selector
|
||||
only offers groups the signed-in user administers, and a user who
|
||||
administers none is told so instead of being shown a form that would fail.
|
||||
|
||||
**One set of rules behind both interfaces.** Every administrative change is
|
||||
a function in `internal/server/admin.go` taking the state and the caller —
|
||||
`createGroup`, `setMember`, `setGrant`, `createInvite`, `revokeInvite`,
|
||||
`adminAccount` and the rest. The JSON handlers and the HTML forms are both
|
||||
thin wrappers over them. Had the pages re-implemented the checks, the two
|
||||
interfaces would have drifted apart on the first change, and that drift would
|
||||
have been a permission bug rather than a cosmetic one.
|
||||
|
||||
**Who may do what.** Group administration follows the tree: administering a
|
||||
group means administering everything inside it, so the member and grant forms
|
||||
appear only on groups you administer, and their selectors only offer vaults
|
||||
you administer. Account changes are deliberately not part of that. They are
|
||||
server administrator only: a group administrator decides who reaches which
|
||||
vault, which is access, while disabling an account, clearing somebody's
|
||||
second factor or deleting them reaches past the group they happen to share.
|
||||
The three changes that would lock the caller out — disable, demote, delete on
|
||||
their own account — are refused outright, because a server with no reachable
|
||||
administrator can only be repaired with `keeporgd -reset-admin` on the host.
|
||||
|
||||
Browser sessions are the same session tokens the API uses, carried in a
|
||||
cookie that is `HttpOnly`, `SameSite=Strict`, `Path=/`, and `Secure` whenever
|
||||
`external_url` is https. Every form carries a per-session CSRF token that is
|
||||
checked on each POST. The group selector only offers groups the signed-in
|
||||
user administers, and a user who administers none is told so instead of being
|
||||
shown a form that would fail.
|
||||
`external_url` is https. Every page is served through `webPage` and every
|
||||
form through `webForm`: they resolve the cookie, and a form additionally has
|
||||
its per-session CSRF token checked before the handler runs. A new form cannot
|
||||
forget that check, because the check is not in the handler.
|
||||
|
||||
No page carries any JavaScript. Each control is its own small form with the
|
||||
CSRF token and one action in it, which is what makes the interface work in a
|
||||
locked-down browser and keeps its attack surface down to that token.
|
||||
|
||||
Creating a vault is one transaction: the server builds an empty KDBX 4
|
||||
database, encrypts it with the password from the form, stores it as version
|
||||
@@ -484,8 +521,9 @@ keeporg invite -group <engineering-id> -role member -uses 5 -hours 72
|
||||
keeporg grant -group <readers-id> -db <db-id> -perm read
|
||||
```
|
||||
|
||||
Or create the vault at `https://keeporg.example.com/web`, which is the same
|
||||
operation with a form around it.
|
||||
Or do all of it at `https://keeporg.example.com/web`: `/web/groups` builds
|
||||
the tree and fills it, `/web/invites` issues the links, `/web/vaults` creates
|
||||
the vault. The pages call the same functions these commands call.
|
||||
|
||||
`keeporg vault` creates the KDBX file itself, so the vault has content from
|
||||
the start: it appears in every member's mirror directory and opens with the
|
||||
|
||||
@@ -121,10 +121,19 @@ the group named or any group above it.
|
||||
| DELETE | `/api/v1/grants` | `?group_id=&database_id=` | database admin |
|
||||
| POST | `/api/v1/invites` | `{group_id, role, ttl_hours, max_uses}` | group admin |
|
||||
| GET | `/api/v1/invites` | – | your own invites, and those of groups you administer |
|
||||
| DELETE | `/api/v1/invites/{token}` | – | whoever may see it: its creator, an admin of its group, a server admin |
|
||||
| GET | `/api/v1/users` | – | yourself, plus members of groups you administer |
|
||||
| PATCH | `/api/v1/users/{id}` | `{action}` | server admin |
|
||||
| GET | `/healthz` | – | unauthenticated liveness |
|
||||
|
||||
`merge_strategy` is one of `merge`, `server-wins`, `client-wins`, `manual`.
|
||||
Switching to `merge` requires a stored key and is otherwise refused with
|
||||
`400`. `perm` is one of `read`, `write`, `admin`. `role` is `member` or
|
||||
`admin`.
|
||||
|
||||
An account `action` is one of `disable`, `enable`, `promote`, `demote`,
|
||||
`clear-totp` or `delete`. `disable` and `delete` also revoke every session
|
||||
that account holds. The three that would lock the caller out — `disable`,
|
||||
`demote`, `delete` — are refused on the caller's own account, because a
|
||||
server with no reachable administrator can only be repaired with
|
||||
`keeporgd -reset-admin` on the host.
|
||||
|
||||
Reference in New Issue
Block a user