Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions book/src/images/roadmap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 19 additions & 5 deletions book/src/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ live on a real deployment before the next. Phase 8 (external auth) is
the main optional, demand-driven work left. For what changed in each
release, see the [release notes](./news.md).

![Roadmap timeline: phases 0–7 are done — 0 to 5 shipped in v0.1.0 (scaffolding, landing page, persistence + admin CRUD, proxy + Docker backend, monitoring dashboard, production polish); phase 6 (multi-host scheduling, app visibility, sub-path mounting) across v0.1.1–v0.1.2; phase 7 (HA / multi-instance) in v0.1.1. Post-1.0 point releases add demo forks, URL-rewrite modernization, unified credentials, the media library, portal logos, admin UX polish, and security + perf fixes. Phase 8 (external auth) is planned and optional.](images/roadmap.svg)
![Roadmap timeline: phases 0–7 are done — 0 to 5 shipped in v0.1.0 (scaffolding, landing page, persistence + admin CRUD, proxy + Docker backend, monitoring dashboard, production polish); phase 6 (multi-host scheduling, app visibility, sub-path mounting) across v0.1.1–v0.1.2; phase 7 (HA / multi-instance) in v0.1.1. Post-phase releases add admin and portal UX, group-scoped Editor delegation, scheduled jobs, viewer-local admin timestamps, timezone-aware cron, and security + reliability fixes. Phase 8 contains only the planned, optional external identity-provider integrations.](images/roadmap.svg)

## Shipped

Expand Down Expand Up @@ -181,6 +181,20 @@ error + exit code instead of a generic "no port binding". Plus catalog
cards expand their description on hover, and the dashboard's
stop/restart actions show in-progress feedback.

**Operations and delegated administration.** The Admin panel now runs
scheduled, run-to-completion ETL jobs with history and alerting. Cron is
evaluated on the IANA wall clock selected by `server.timezone`, preserving
UTC when the setting is absent; the Schedules page uses that same clock.
Other persisted admin timestamps stay in UTC and are converted in the
browser to each viewer's timezone.

Editor delegation is row-scoped from current group memberships across
Applications, Users, and Groups. Open apps remain available to every
Editor, Media is shared, and an out-of-scope target returns `404`.
Admins and the break-glass token remain unrestricted. Account deletion,
CSV user import, MFA reset, and creating, renaming, or deleting a group
remain Admin-only.

## Planned (optional)

Demand-driven — Ruscker is complete and useful without it.
Expand All @@ -206,10 +220,10 @@ Tracked in the GitHub issues; picked up as real usage asks for them:
long-session flap has since shipped.)*

### Phase 8 — External auth
OIDC (Keycloak / Auth0 / Google), SAML, and LDAP, plus per-app access
lists ("only group X can use this app"). The coarse Viewer / Editor /
Admin RBAC already shipped in Phase 5; this is the federated-identity
and fine-grained-ACL layer on top.
OIDC (Keycloak / Auth0 / Google), SAML, and LDAP. Per-app access lists
and the Viewer / Editor / Admin model already ship, including
group-scoped Editor delegation across Applications, Users, and Groups.
This phase is only the federated-identity layer on top.

## Explicitly out of scope

Expand Down
42 changes: 34 additions & 8 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ per request. User/group/profile mutations invalidate the cache with a
generation counter, preventing an in-flight stale read from repopulating
revoked identity data.

Admin-panel authorization keeps identity and row scope separate:
`crate::auth` answers who the caller is and which role the session carries;
`scope::EditorScope` answers which Application, User, and Group rows that
caller may see or change. It refetches an Editor's groups on every request
and fails closed; an out-of-scope target looks absent (`404`). Admin and
break-glass sessions are unrestricted, open apps are shared by all Editors,
and Media is shared. Account deletion, CSV user import, MFA reset, and group
creation/rename/deletion remain Admin-only.

The request guard then applies these boundaries in order:

1. `Spec::access_allows` enforces per-user/per-group access server-side.
Expand All @@ -127,10 +136,16 @@ Postgres:

`jobs::spawn` starts one scheduler loop per process when both a catalog DB
and container backend exist (the local Docker backend runs jobs; the
multi-host backend does not, so a due schedule there records an error). Every 30 seconds it loads enabled `schedules`;
only the `LeaderElector` winner may fire in HA. `db::schedules::mark_fired`
atomically advances `last_run_at` before execution, so a split-brain second
runner loses the claim and a crash does not double-fire the occurrence.
multi-host backend does not, so a due schedule there records an error).
Every 30 seconds it loads enabled `schedules`; only the `LeaderElector`
winner may fire in HA. Cron expressions are evaluated against the IANA wall
clock named by `server.timezone`; when it is absent, the historical UTC
behaviour is preserved. An invalid name also falls back to UTC with a
validation warning. The Schedules page calls the same `next_occurrence`
function and renders firing times in that operational zone.
`db::schedules::mark_fired` atomically advances `last_run_at` before
execution, so a split-brain second runner loses the claim and a crash does
not double-fire the occurrence.

A new schedule anchors at `created_at` (no fire-on-create). If downtime spans
several cron occurrences, the next tick collapses them to one firing. The job
Expand All @@ -141,6 +156,15 @@ per-schedule timeout defaults to one hour in the backend. Results, duration,
exit code, and log tail land in `schedule_runs`; failures enqueue the
`job-failed` alert webhook.

These are deliberately two different time models. Stored instants remain
UTC. Ordinary admin tables emit those instants as `<time datetime="…Z">`;
the shared layout converts them in the browser to the viewer's timezone and
keeps UTC as the no-JavaScript fallback. Schedules stay on the configured
operational clock because their timestamps explain when cron will fire.
Changing `server.timezone` on an installation with existing schedules can
produce one extra run on the switch-over day, so operators should review
their next-run values after adopting a zone.

### Aggregated access counter

`access_counter::AccessCounter` keeps the proxy hot path free of database
Expand Down Expand Up @@ -298,7 +322,8 @@ SQLite or the HA Postgres catalog.
replica up and down. Set `min-replicas: 1`+ to keep an app warm.
- The session-purger runs as a periodic task (every 5s).
- The leader-only job scheduler checks cron schedules every 30s and detaches
each run-to-completion job so long ETL work cannot block later ticks.
each run-to-completion job so long ETL work cannot block later ticks. It
evaluates cron on the `server.timezone` wall clock (UTC when unset).
- The access-counter drain batches in-memory deltas every 2s instead of
writing on every proxy request.
- `DashMap` backs the replica/in-flight state and the short-TTL identity/spec
Expand All @@ -312,9 +337,10 @@ SQLite or the HA Postgres catalog.
Admin paths require an authenticated session.
- **Privileged**: signed-in users. Authentication uses per-user passwords
and three roles — **Viewer** (portal access only; no admin section),
**Editor** (dashboard, apps, and media), **Admin** (everything, incl. user
management) — enforced server-side. A break-glass `RUSCKER_ADMIN_TOKEN`
bootstraps the first account. See `docs/SECURITY.md` §2.
**Editor** (dashboard plus group-scoped Applications, Users, and Groups;
open apps and Media are shared), **Admin** (unrestricted) — enforced
server-side. A break-glass `RUSCKER_ADMIN_TOKEN` bootstraps the first
account and is also unrestricted. See `docs/SECURITY.md` §2.
- **Operator**: filesystem access (the person running Ruscker). Can
edit YAML, restart the process.

Expand Down
7 changes: 6 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ drill into any container.
- [ ] LDAP directory integration
- [x] Role-based access control (Viewer / Editor / Admin), including
group-scoped Editor delegation across Apps, Users and Groups; Admin
accounts and the break-glass token remain unrestricted
accounts and the break-glass token remain unrestricted, open apps and
Media stay shared, and out-of-scope row targets return 404
- [x] Per-spec access lists (only group X can use this app) — shipped in Phase 6
- [x] Per-app step-up MFA — one user-owned TOTP factor, recovery codes,
trusted-device grants, and per-spec proof-freshness policy
Expand All @@ -169,9 +170,13 @@ drill into any container.

- [x] Scheduled jobs — Admin **Schedules** page, cron ETL semantics,
no fire-on-create, collapsed downtime catch-up, leader-only HA firing,
IANA `server.timezone` evaluation with the historical UTC default,
the spec's image/environment/volumes/credentials, optional command
override, run history/log tail, one-hour default timeout, and the
`job-failed` alert webhook
- [x] Viewer-local admin timestamps — persisted UTC instants are emitted as
machine-readable `<time>` values and converted in the browser, while
scheduled firing times remain on the scheduler's configured clock
- [x] Named-volume management — Admin Disk **Volumes** card with live
reference counts; create labels Ruscker-owned volumes, and remove is
offered only for Ruscker-created volumes with zero live references and
Expand Down
12 changes: 6 additions & 6 deletions docs/images/roadmap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.