feat(cli): merge oxicloud binary and cli
this feature to simplify the creation of only 1 binary for multiple architecture
This commit is contained in:
@@ -26,7 +26,7 @@ probe, and lifecycle behaviour are uniform.
|
||||
|
||||
Every entry is declared in `OXICLOUD_STORAGE_ENTRIES` (comma-separated
|
||||
list of names). The active entry is stored in `admin_settings` and
|
||||
switched via `oxicloud --select-storage <name>` on the command line
|
||||
switched via `oxicloud storage select <name>` on the command line
|
||||
or automatically at the end of a successful `backend_migration`.
|
||||
Non-active entries stay reachable through the multi-entry API (test,
|
||||
audit, migrate-into).
|
||||
@@ -119,7 +119,7 @@ Rendered visually via `xxd -l 15 <blob>`:
|
||||
|
||||
Fingerprints are rendered the same colon-hex form (`15:f3:…:50`)
|
||||
everywhere they appear: boot log, admin panel pair chain, `xxd`
|
||||
inspection, `oxicloud --fingerprint <base64>` CLI, and the rotate /
|
||||
inspection, `oxicloud storage fingerprint <base64>` CLI, and the rotate /
|
||||
migration audit lines. That means an admin can cross-reference by
|
||||
eye — same string means same key.
|
||||
|
||||
@@ -336,7 +336,7 @@ readonly (source stays active — writes safe there), and returns
|
||||
`RunOutcome::Failed`. Operator inspects findings, then either
|
||||
retries (walk short-circuits on head-format matches → cheap
|
||||
re-attempt), fixes the source, or explicitly accepts the partial
|
||||
via `oxicloud --select-storage <target>`.
|
||||
via `oxicloud storage select <target>`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Runs are recoverable — status, cursor, and per-blob failure findings all live
|
||||
If an entry is renamed or removed from `.env` while the DB pointer still names the old one, boot aborts with a clear error pointing at:
|
||||
|
||||
```
|
||||
oxicloud --select-storage <name>
|
||||
oxicloud storage select <name>
|
||||
```
|
||||
|
||||
This one-shot repair command re-runs the same env-parse the server does at boot, verifies `<name>` is declared in `OXICLOUD_STORAGE_ENTRIES`, updates `admin_settings.storage.active_backend_name` in the DB, and exits. Operator then restarts normally. See [Environment Variables — Storage Entries](/config/env#storage-entries-multi-entry-recommended) for the model, and [`oxicloud --help`](https://github.com/oxicloud/oxicloud/blob/main/src/main.rs) for the full flag list.
|
||||
|
||||
@@ -135,12 +135,12 @@ Password-using deployments will opt in via three env vars:
|
||||
2. **`OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`** — generated once and persisted like your JWT secret. Rotating this invalidates every user's registration; treat it as one of the crown jewels. Two ways to generate:
|
||||
```bash
|
||||
# Docker (recommended in production — no toolchain needed):
|
||||
docker run --rm ghcr.io/atalayalabs/oxicloud:latest oxicloud-cli opaque setup
|
||||
docker run --rm ghcr.io/atalayalabs/oxicloud:latest oxicloud opaque setup
|
||||
|
||||
# From a source checkout:
|
||||
cargo run --bin oxicloud-cli -- opaque setup
|
||||
cargo run --bin oxicloud -- opaque setup
|
||||
```
|
||||
Both print the base64 value on stdout (with guidance on stderr, so shell pipelines like `$(docker run ... oxicloud-cli opaque setup)` capture cleanly).
|
||||
Both print the base64 value on stdout (with guidance on stderr, so shell pipelines like `$(docker run ... oxicloud opaque setup)` capture cleanly).
|
||||
3. **`OXICLOUD_AUTH_OPAQUE_KSF_*`** — client-side Argon2id key-stretching cost. Defaults (46 MiB / 1 iter / 1 lane) match OWASP's interactive-auth recommendation. See the next section for the rationale + when to bump.
|
||||
|
||||
The `OXICLOUD_HASH_*` variables (server-side legacy Argon2) and `OXICLOUD_AUTH_OPAQUE_KSF_*` (client-side OPAQUE Argon2) are intentionally separate: the server-side path is RAM-bounded by concurrent-login traffic and needs to stay modest; the client-side path is single-user per attempt and can be tuned independently. Tuning them together would force a bad compromise in one direction or the other.
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ OPAQUE (RFC 9807) is a zero-knowledge password-authenticated key exchange: the p
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `OXICLOUD_AUTH_OPAQUE_MODE` | `off` | Runtime mode. `off` = endpoints 404 (default). `migrate` = endpoints live, legacy `POST /api/auth/login` still accepted. `opaque_only` = endpoints live, legacy refused for users with an envelope. **Effective-mode cross-check**: when `password` is not in `OXICLOUD_AUTH_METHODS`, the mode is auto-downgraded to `off` with an audit-channel INFO line (OPAQUE only replaces the password path — nothing to shadow in an OIDC-only or magic-link-only deployment). So OIDC / magic-link-only operators can safely ignore every `OXICLOUD_AUTH_OPAQUE_*` variable. |
|
||||
| `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` | — | Base64-encoded `ServerSetup` blob. **Required** when `OXICLOUD_AUTH_OPAQUE_MODE != off` AND password is enabled — the server refuses to start with a helpful error otherwise. Generate once with the `oxicloud-cli opaque setup` subcommand and persist the value like your JWT secret. **Never rotate** — rotating invalidates every user's envelope (they'd all need to reset their passphrase). |
|
||||
| `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` | — | Base64-encoded `ServerSetup` blob. **Required** when `OXICLOUD_AUTH_OPAQUE_MODE != off` AND password is enabled — the server refuses to start with a helpful error otherwise. Generate once with the `oxicloud opaque setup` subcommand and persist the value like your JWT secret. **Never rotate** — rotating invalidates every user's envelope (they'd all need to reset their passphrase). |
|
||||
| `OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB` | `47104` | Client-side Argon2id memory cost in KiB (46 MiB — matches OWASP interactive-auth recommendation). Runs on the user's device during OPAQUE login/registration, TWICE per login. Distinct from `OXICLOUD_HASH_MEMORY_COST` (server-side legacy path). Bumping raises brute-force cost after a hypothetical envelope leak but also raises login latency and risks WASM heap OOM on low-memory devices — see `authentication.md § OPAQUE — KSF parameters` for the full rationale + per-device latency table. |
|
||||
| `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS` | `1` | Client-side Argon2id iteration count (OWASP interactive-auth recommendation). |
|
||||
| `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM` | `1` | Client-side Argon2id parallelism lanes (OWASP recommendation). Higher only helps on multi-core hardware and can hurt single-core / older mobile devices. |
|
||||
@@ -131,7 +131,7 @@ Each declared name `<N>` then reads its own set of per-entry variables:
|
||||
|
||||
- A declared name whose required per-entry fields are missing (`_BACKEND` never set, S3 with no `_S3_BUCKET`, Azure with no `_AZURE_CONTAINER`).
|
||||
- Setting `OXICLOUD_STORAGE_ENTRIES` alongside any of the legacy flat vars below (`OXICLOUD_STORAGE_BACKEND`, `OXICLOUD_S3_*`, `OXICLOUD_AZURE_*`, `OXICLOUD_STORAGE_ENCRYPTION_*`). Pick one mode; the error lists every conflicting var to remove.
|
||||
- A DB pointer (`admin_settings.storage.active_backend_name`) that names an entry not in the current `_ENTRIES`. The error points at the repair flag `oxicloud --select-storage <name>` — verify + UPDATE DB + exit.
|
||||
- A DB pointer (`admin_settings.storage.active_backend_name`) that names an entry not in the current `_ENTRIES`. The error points at the repair flag `oxicloud storage select <name>` — verify + UPDATE DB + exit.
|
||||
|
||||
**Example** — two entries, local disk plus an S3 target for planned migration:
|
||||
|
||||
|
||||
@@ -167,13 +167,13 @@ If you rename or remove a backend from `.env` while it was still the active one,
|
||||
```
|
||||
active_backend_name = `s3_prod`, but no entry with that name is declared in
|
||||
OXICLOUD_STORAGE_ENTRIES. Available: [local_main]. […]
|
||||
oxicloud --select-storage <one-of-the-available-names>
|
||||
oxicloud storage select <one-of-the-available-names>
|
||||
```
|
||||
|
||||
Run the command it suggests to pick a still-declared backend and the server will boot again on the next start:
|
||||
|
||||
```
|
||||
oxicloud --select-storage local_main
|
||||
oxicloud storage select local_main
|
||||
```
|
||||
|
||||
This just updates which backend OxiCloud considers active — it doesn't move any data.
|
||||
|
||||
@@ -41,7 +41,9 @@ Mirrors the `tests/api/` shell pattern (run.sh, test.env, separate server port).
|
||||
|
||||
### 2. Rust bulk seeder — `src/bin/load-seed.rs`
|
||||
|
||||
New binary registered in `Cargo.toml` alongside `generate-openapi` and `migrate-nfc-filenames`.
|
||||
New binary registered in `Cargo.toml` alongside `generate-openapi`
|
||||
(the historical `migrate-nfc-filenames` bin has since been folded into
|
||||
`oxicloud migrate nfc-filenames` — see `docs/plan/bundled-binary.md` § 1b).
|
||||
|
||||
**CLI:**
|
||||
```
|
||||
@@ -158,7 +160,7 @@ Matches existing recipe naming (`test-*`, `front-*`, `api-test`).
|
||||
- `.github/workflows/load-nightly.yml`, `load-smoke.yml`
|
||||
|
||||
**Modify:**
|
||||
- `Cargo.toml` — add `[[bin]] name = "load-seed" path = "src/bin/load-seed.rs"` after the `migrate-nfc-filenames` entry
|
||||
- `Cargo.toml` — add `[[bin]] name = "load-seed" path = "src/bin/load-seed.rs"` after the `generate-openapi` entry (the `migrate-nfc-filenames` bin referenced in earlier drafts has been folded into `oxicloud migrate nfc-filenames`)
|
||||
- `justfile` — append four `load*` recipes
|
||||
- `.gitignore` — add `tests/load/results/*.json` and `tests/load/storage/`
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ authenticated session):
|
||||
|
||||
**Deferred:**
|
||||
- Step-up auth before link start
|
||||
- Admin-mediated link/unlink via `oxicloud-cli federation` (proper for
|
||||
- Admin-mediated link/unlink via `oxicloud federation` (proper for
|
||||
"user changed IdP email" recovery scenario)
|
||||
- OCM link (same shape, different kind)
|
||||
- Multi-federation (multiple linked identities per user — see
|
||||
|
||||
@@ -62,7 +62,7 @@ and ordering are the load-bearing decisions here.
|
||||
## Preconditions before we start the wipe
|
||||
|
||||
Every one of these MUST hold. Adding a pre-flight check in
|
||||
`oxicloud-cli opaque wipe-legacy` (proposed below) that refuses to run
|
||||
`oxicloud opaque wipe-legacy` (proposed below) that refuses to run
|
||||
otherwise.
|
||||
|
||||
1. **`OXICLOUD_AUTH_OPAQUE_MODE=opaque_only`** on the deployment for at
|
||||
@@ -174,7 +174,7 @@ it can't, since login-link users just clicked email — no proof-of-current).
|
||||
|
||||
### The wipe migration
|
||||
|
||||
Delivered as `oxicloud-cli opaque wipe-legacy` — a dedicated subcommand,
|
||||
Delivered as `oxicloud opaque wipe-legacy` — a dedicated subcommand,
|
||||
NOT a schema migration. Reasons:
|
||||
- Idempotent (won't re-wipe already-nulled rows)
|
||||
- Pre-flight refuses when preconditions aren't met (unlike a migration
|
||||
@@ -207,7 +207,7 @@ UPDATE auth.users
|
||||
|
||||
Output: `N password_hash columns nulled. M users still have password_hash
|
||||
because they don't meet the OPAQUE-migrated preconditions — inspect via
|
||||
`oxicloud-cli opaque wipe-legacy --dry-run` and address separately.`
|
||||
`oxicloud opaque wipe-legacy --dry-run` and address separately.`
|
||||
|
||||
The `WHERE` clause is intentionally strict: OIDC users, externals, and
|
||||
under-migrated users are ALL left alone. The strict version is safer than
|
||||
@@ -232,7 +232,7 @@ can drop the legacy password code:
|
||||
6. `has_password` field on `UserDto` / `AdminUserSummaryDto`: delete (always
|
||||
false, meaningless signal)
|
||||
7. `admin`-badge `password` chip: delete (same reason)
|
||||
8. `oxicloud-cli opaque reset --user X` for legacy-recovery: still useful
|
||||
8. `oxicloud opaque reset --user X` for legacy-recovery: still useful
|
||||
as an emergency lever (envelope somehow corrupted, need to force
|
||||
re-registration via recovery-magic-link), but its "silent-migration
|
||||
handles the recovery" semantics become "recovery-magic-link handles the
|
||||
@@ -269,7 +269,7 @@ running smoothly for the indicated period."
|
||||
| G1 | Land task #31: change_password OPAQUE-lockout fix + hybrid-user password gate | Days |
|
||||
| G2 | Land recovery-magic-link admin reset flow | Weeks |
|
||||
| G3 | Land OPAQUE-verify-current + change_password redesign that COMPOSES the two (Argon2-verify AND OPAQUE-verify both work; use whichever the user has) | Weeks |
|
||||
| G4 | Ship `oxicloud-cli opaque wipe-legacy` (dry-run only initially, no destructive flag) | Days |
|
||||
| G4 | Ship `oxicloud opaque wipe-legacy` (dry-run only initially, no destructive flag) | Days |
|
||||
| G5 | Add admin-dashboard metric: "N users still on legacy (`password_hash IS NOT NULL AND !opaque_migrated`)" | Days |
|
||||
| G6 | Operator switches deployment to `opaque_only` mode | ✅ already possible |
|
||||
| G7 | Wait 90+ days at `opaque_only`, watch the metric drop to 0 | Months |
|
||||
|
||||
@@ -324,7 +324,7 @@ at), boot fails fast with a clear error. Operator has two ways out:
|
||||
restart.
|
||||
2. **CLI repair flag on the `oxicloud` binary itself**:
|
||||
```
|
||||
oxicloud --select-storage <name>
|
||||
oxicloud storage select <name>
|
||||
```
|
||||
Behaviour: parse `.env`, verify `<name>` exists in `_ENTRIES` (fail-fast
|
||||
with the available names listed if not), connect to DB, UPDATE
|
||||
@@ -334,7 +334,7 @@ at), boot fails fast with a clear error. Operator has two ways out:
|
||||
|
||||
The bare-flag on the shipped binary is chosen over a separate `just`
|
||||
recipe or auxiliary bin because:
|
||||
- **Docker-friendly**: `docker exec oxicloud oxicloud --select-storage foo`
|
||||
- **Docker-friendly**: `docker exec oxicloud oxicloud storage select foo`
|
||||
— no need to install extra tooling in the container.
|
||||
- **Systemd-friendly**: can be run as a `ExecStartPre=` one-shot before the
|
||||
main service unit.
|
||||
@@ -377,7 +377,7 @@ foundational; the rest layer on top independently within reason.
|
||||
| 5 | Cutover state machine: on migration `Completed`, write `active_backend_name = target_name`, keep read-only on. Boot on new backend after operator restart. | 4 | ~half day |
|
||||
| 6 | Admin storage tab rewrite: list entries, show active, migrate dropdown, read-only banner. Delete Save form + S3 field editors + .env cutover hint. | 1, 3, 4 | 1 day |
|
||||
| 7 | `?storage=<name>` on `blobs_consistency` + `backend_consistency`. `JobRunArgs.storage` plumbing, `TriggerJobQuery.storage`, entry-resolver at run start, params records probed name. Retire `verify_migration` + its DTO + its route + its handler. | 1, 3 | 1 day |
|
||||
| 8 | `oxicloud --select-storage <name>` bare-flag repair command on the main binary. Parses `.env`, verifies entry exists, UPDATEs DB, exits. Boot-time missing-entry error message points at it. See §Fallback. | 2 | ~quarter day |
|
||||
| 8 | `oxicloud storage select <name>` bare-flag repair command on the main binary. Parses `.env`, verifies entry exists, UPDATEs DB, exits. Boot-time missing-entry error message points at it. See §Fallback. | 2 | ~quarter day |
|
||||
|
||||
**Total: ~5-6 days end to end.** Slices 6 and 7 can proceed in parallel with
|
||||
each other once 1-5 land. Slice 8 is an ops nicety, could ship whenever.
|
||||
@@ -414,8 +414,8 @@ Per slice, plus these end-to-end scenarios in Hurl:
|
||||
→ 400 with known-names list. No run row created.
|
||||
9. **Missing entry at boot**: `active_backend_name = "gone"` but `_ENTRIES`
|
||||
doesn't include it → boot aborts with the specific message pointing at
|
||||
`oxicloud --select-storage <name>` (with the available names filled in).
|
||||
Re-run the binary with `--select-storage local_main` → verifies + updates
|
||||
`oxicloud storage select <name>` (with the available names filled in).
|
||||
Re-run the binary with `storage select local_main` → verifies + updates
|
||||
DB + exits 0. Restart the server → boots cleanly on `local_main`.
|
||||
10. **Encryption key invalid**: `OXICLOUD_STORAGE_<N>_ENCRYPTION_KEY=badbase64`
|
||||
→ boot aborts with entry name + reason (not valid base64 / wrong length).
|
||||
|
||||
Reference in New Issue
Block a user