# Multi-user RBAC (https://portunus.bybee.dev/en/docs/features/rbac)



Available since v0.5.0. Replaces the single-operator model with a
proper RBAC layer above the unchanged data plane.

## Concepts [#concepts]

* **User** — id (lower-snake `[a-z][a-z0-9_-]{0,31}`), role
  (`superadmin` / `user`), display name, optional `disabled` flag.
  Reserved `_`-prefix is rejected via the public constructor.
* **Password** — since v1.1.0, Web UI login uses a local password hashed in
  PHC/Argon2id format. Admins create users manually and can require a password
  change on first login.
* **Credential** — a bearer API token tied to a user. `blake3`-hashed at
  rest; raw token visible only at issuance / rotation. Carries an
  optional label, status (`active` / `revoked`), and `last_used_at`.
* **Grant** — per-user authorisation triple `{client, listen-port range,
  protocol set}`. `client` is either a named `ClientName` or wildcard
  `"*"`.

## First admin [#first-admin]

Since v1.1.0, a fresh store is initialized through the Web UI:

1. Start `portunus-server serve`.
2. Read the one-time setup token from the server log / stderr.
3. Open the Web UI and create the first `superadmin` with a local password.

The setup token expires after 30 minutes. If onboarding is not completed, each
server process start rotates the token and invalidates the previous one. After
the first active `superadmin` exists, onboarding stays closed.

The older bearer bootstrap paths remain available for automation and upgrade
compatibility.

### Path A — `bootstrap-superadmin` [#path-a--bootstrap-superadmin]

```sh
portunus-server --data-dir ./srv bootstrap-superadmin --name ops
# → superadmin user_id=_superadmin token=<paste-into-PORTUNUS_OPERATOR_TOKEN>
```

The bearer prints **exactly once**. Idempotent — refuses to bootstrap
twice and exits non-zero.

### Path B — server.toml shortcut [#path-b--servertoml-shortcut]

```toml
operator_token = "<43-char URL-safe-base64 token>"
```

On first start with no superadmin present, the server auto-mints a
reserved `_legacy` superadmin keyed to that token. Idempotent — leave
the line in across restarts.

Generate a token out-of-band:

```sh
portunus-server gen-token
# → 43-char URL-safe-base64
```

These paths create API-token access, not a browser password. For the Web UI,
prefer the onboarding flow.

## Add a constrained tenant [#add-a-constrained-tenant]

```sh
portunus-server user-add alice --display-name Alice
portunus-server credential-issue alice --label laptop
# → bearer token printed once, store securely

portunus-server grant-add --user-id alice --client edge-01 \
  --listen-port-start 30000 --listen-port-end 30050 \
  --protocols tcp,udp
```

Now alice can `push-rule` only on `edge-01`, only on ports
`30000..=30050`, only TCP or UDP.

In the Web UI, a `superadmin` can create the same user with an initial password
and force-change flag. Public self-registration is intentionally not supported,
because grants are operator-managed.

## Closed-set matching [#closed-set-matching]

A single grant must cover the **entire** requested listen range — rules
straddling two grants are rejected. Outside the envelope, alice gets
HTTP 403 with one of:

* `client_not_granted`
* `port_outside_grant`
* `protocol_not_granted`

A `--client *` grant matches any client. A grant whose
`--listen-port-end` equals `--listen-port-start` is a single-port grant.

## API token rotation [#api-token-rotation]

A user can rotate their own credential without superadmin help:

```sh
PORTUNUS_OPERATOR_TOKEN=<alice's old token> \
  portunus-server credential-rotate alice <credential_id>
# → new bearer printed once; old bearer 401s on next request
```

## Read-side filtering [#read-side-filtering]

`GET /v1/rules` projects only the caller's owned rules to non-superadmin
users. Superadmin gets `?owner=<user_id>` to filter by owner. Every
rule response carries an `owner` field stamped at push time.

## Audit log [#audit-log]

Every operator request emits one structured `event = "operator.allow"`
(INFO) or `"operator.deny"` (WARN) line carrying `actor`, `method`,
`path`, `outcome`, and (on deny) the `RbacError::code()` reason.

**Raw bearer tokens never reach the audit code path** — the audit
emitter takes only the post-verify `OperatorIdentity`.

Password-reset audit entries use `action = "operator.password_reset"` and record
actor, target user, outcome, and whether Web sessions / API tokens were
revoked. Raw passwords, temporary passwords, setup tokens, and bearer tokens are
never written to audit.

## Password recovery [#password-recovery]

Normal users do not self-reset through a public "forgot password" form. A
`superadmin` resets passwords from the Web UI. Resetting revokes Web sessions
and revokes API tokens by default unless the admin explicitly keeps them.

If the last `superadmin` loses the password, stop the server and run the local
break-glass command on the host that owns the data dir. Use the actual
superadmin user ID. For `bootstrap-superadmin` installs that ID is
`_superadmin`; for Web onboarding it is the ID chosen during setup, for example
`admin`.

```sh
portunus-server --data-dir /var/lib/portunus reset-password admin --temporary
```

The command prints a temporary password once and requires a password change at
next Web login. It never creates a new `superadmin` and there is no remote reset
backdoor.

## CLI surface [#cli-surface]

| Command                                                                            | Purpose                                      |
| ---------------------------------------------------------------------------------- | -------------------------------------------- |
| `user-add` / `user-list` / `user-get` / `user-remove`                              | Manage users                                 |
| `reset-password <user_id>`                                                         | Local password recovery against the data dir |
| `credential-issue` / `credential-list` / `credential-revoke` / `credential-rotate` | Manage credentials                           |
| `grant-add` / `grant-list` / `grant-revoke`                                        | Manage grants                                |

All take the operator token from `PORTUNUS_OPERATOR_TOKEN`
(exit 4 if missing).

## SIGHUP reload [#sighup-reload]

On `SIGHUP` the operator-store reloads `identity.json` (or the SQLite
state, since v0.8) from disk. On validation failure the prior in-memory
snapshot is kept; one structured log line is emitted either way.

## Cascade ordering [#cascade-ordering]

On user removal or grant revocation, identity flush commits **first**,
then dependent rules are removed. A crash mid-cascade leaves a coherent
identity state.

**Last-superadmin protection** refuses the removal that would orphan
the cluster.
