Skip to content

Commit e39714c

Browse files
committed
docs(java): dashboard auth, OAuth/SSO, Spring, webhooks
Documents session auth (first-run setup, env-admin, RBAC, CSRF), the OAuth/OIDC providers + env config (new sso guide), the Taskito.dashboard() convenience + CLI flags, the Spring dashboard auto-configuration, metrics/probes, and the dashboard webhook management + SSRF guard. Corrects now-stale security notes.
1 parent 48333b1 commit e39714c

6 files changed

Lines changed: 552 additions & 45 deletions

File tree

docs/content/docs/java/guides/extensibility/webhooks.mdx

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,75 @@ async `POST` with a JSON body in snake_case:
6161
A send error or a `5xx` response is retried with exponential backoff (500 ms
6262
base, doubling, capped at 30 s) up to `maxRetries`; `4xx` responses are not
6363
retried. Verify the signature on your endpoint by HMAC-SHA256-ing the raw body
64-
with the shared secret and comparing against the `X-Taskito-Signature` header
65-
in constant time.
64+
with the shared secret's raw UTF-8 bytes and comparing against the
65+
`X-Taskito-Signature` header in constant time.
6666

6767
<Callout type="info">
68-
Delivery is fire-and-forget: there is no persisted per-delivery log to query
69-
— final failures are logged (with the URL's path redacted). Subscriptions
70-
themselves are persisted in the queue's settings store, so every process
71-
sharing the backend sees the same hooks; changes propagate to running
72-
workers within 30 seconds.
68+
Every attempt — successful or not — is recorded in a persistent,
69+
per-subscription delivery log (see [Dashboard
70+
management](#dashboard-management) below); final failures are also logged
71+
server-side (with the URL's path redacted). Subscriptions themselves are
72+
persisted in the queue's settings store, so every process sharing the
73+
backend sees the same hooks; changes propagate to running workers within 30
74+
seconds.
7375
</Callout>
76+
77+
## SSRF guard
78+
79+
Webhook URLs are vetted by `WebhookUrlValidator` before dashboard-submitted
80+
subscriptions are stored, and **every delivery re-validates the URL right
81+
before sending** — regardless of how the subscription was created — so a
82+
hostname that starts safe and is later rebound to an internal address (DNS
83+
rebinding) is still refused. Blocked by default:
84+
85+
- Non-`http`/`https` schemes
86+
- `localhost` and `*.localhost` / `*.local` / `*.internal` / `*.intranet` /
87+
`*.lan` / `*.private`
88+
- Any address that resolves to loopback, link-local, RFC1918 (site-local),
89+
multicast, carrier-grade NAT (`100.64.0.0/10`), or IPv6 unique-local
90+
(`fc00::/7`) — including cloud metadata endpoints like `169.254.169.254`
91+
(link-local)
92+
93+
Set `TASKITO_WEBHOOKS_ALLOW_PRIVATE` (`1`/`true`/`yes`/`on`) to lift the guard
94+
for local development against `http://localhost`. Keep it unset in
95+
production.
96+
97+
<Callout type="warning">
98+
`WebhookManager.create(...)` called directly from your own code is trusted
99+
developer input and is <b>not</b> pre-validated at creation time — only
100+
dashboard-submitted URLs are checked at creation. Every delivery, from
101+
either surface, is always re-validated regardless.
102+
</Callout>
103+
104+
## Dashboard management
105+
106+
The same subscriptions this page describes are also manageable from the
107+
dashboard's Webhooks page, or directly over its REST API — full CRUD,
108+
delivery history, secret rotation, and a synchronous test-ping. It's the same
109+
`WebhookManager` under the hood (`WebhookManager.forQueue`, backed by the
110+
queue's settings store), so changes from either surface are immediately
111+
visible to every process sharing the backend.
112+
113+
| Method · Path | Effect |
114+
|---|---|
115+
| `GET /api/webhooks` · `/{id}` | List / fetch subscriptions. Header values and the secret are masked — only a `has_secret` flag is returned. |
116+
| `POST /api/webhooks` | Create. Same fields as the builder, plus `generate_secret: true` to mint one server-side. |
117+
| `PUT /api/webhooks/{id}` | Partial update — only included fields change. |
118+
| `DELETE /api/webhooks/{id}` | Delete the subscription and its delivery log. |
119+
| `POST /api/webhooks/{id}/test` | Synchronously deliver a synthetic `test` event (single attempt, no retry) and record the outcome. |
120+
| `POST /api/webhooks/{id}/rotate-secret` | Mint a fresh secret and persist it. |
121+
| `GET /api/webhooks/{id}/deliveries` | Paged delivery log — `?status=&event=&limit=&offset=` (max 200). |
122+
| `GET /api/webhooks/{id}/deliveries/{deliveryId}` | A single delivery. |
123+
| `POST /api/webhooks/{id}/deliveries/{deliveryId}/replay` | Re-fire a stored payload as a fresh delivery (single attempt); the original record is kept. |
124+
125+
The raw secret is returned exactly once — on `create` (when set or
126+
generated) and on `rotate-secret` — never on `list`/`get`.
127+
128+
### Delivery history
129+
130+
Each subscription keeps its most recent 200 deliveries in a FIFO log
131+
(`webhooks:deliveries:<subscriptionId>` in the settings store), newest-first
132+
when paged. Each record carries the event type, `status`
133+
(`delivered`/`failed`), attempt count, response code, a response body
134+
truncated to 2 KiB, latency, and any transport error — enough to debug a
135+
failing endpoint without leaving the dashboard.

docs/content/docs/java/guides/integrations/spring.mdx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,35 @@ Taskito taskito(TaskitoProperties properties) {
6868
example in an `ApplicationRunner` or a `SmartLifecycle` bean) and register
6969
your handlers on it.
7070
</Callout>
71+
72+
## Dashboard auto-configuration
73+
74+
Set `taskito.dashboard.enabled=true` to auto-start a
75+
[`DashboardServer`](/java/guides/operations/dashboard) bean over the
76+
`Taskito` bean. It's stopped with the application context
77+
(`destroyMethod = "close"`) and backs off (`@ConditionalOnMissingBean`) if you
78+
define your own `DashboardServer` bean.
79+
80+
```yaml
81+
taskito:
82+
url: postgres://localhost/taskito
83+
dashboard:
84+
enabled: true
85+
port: 8080
86+
# token: ${DASH_TOKEN:} # omit for session auth (default); set to use legacy shared-token mode
87+
# static-dir: /opt/taskito/dashboard-static
88+
# secure-cookies: true # false drops the Secure cookie attribute for local HTTP
89+
```
90+
91+
| Property | Type | Default | Description |
92+
|---|---|---|---|
93+
| `taskito.dashboard.enabled` | `boolean` | `false` | Auto-start the dashboard server |
94+
| `taskito.dashboard.port` | `int` | `8080` | Bind port (`0` for ephemeral) |
95+
| `taskito.dashboard.token` | `String` | none | Legacy shared token gating `/api/*`; unset enables [session auth](/java/guides/operations/dashboard#session-auth-default) |
96+
| `taskito.dashboard.static-dir` | `String` | auto-discovered | Directory of a prebuilt SPA, overriding the jar's extracted copy |
97+
| `taskito.dashboard.secure-cookies` | `boolean` | `true` | Drop the `Secure` cookie attribute for local HTTP development |
98+
99+
OAuth/OIDC env vars (`TASKITO_DASHBOARD_OAUTH_*`, see
100+
[SSO](/java/guides/operations/sso)) and the admin bootstrap
101+
(`TASKITO_DASHBOARD_ADMIN_USER`/`_PASSWORD`) are read the same way regardless
102+
of Spring — they aren't Spring properties.
Lines changed: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
---
22
title: Dashboard
3-
description: "Serve the bundled web dashboard and its REST API from a Java process."
3+
description: "Serve the bundled web dashboard and its REST API — session auth, OAuth/SSO, and legacy token mode."
44
---
55

66
The jar bundles the web dashboard SPA; `DashboardServer` serves it plus a JSON
77
REST API over the queue — no separate service, no asset build step.
88

9+
## Starting the dashboard
10+
11+
<Tabs items={["Programmatic", "Taskito.dashboard()", "CLI"]}>
12+
<Tab value="Programmatic">
13+
914
```java
1015
try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open();
1116
DashboardServer server = DashboardServer.start(taskito, 8080)) {
@@ -14,60 +19,178 @@ try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open();
1419
}
1520
```
1621

17-
Or from the [CLI](/java/guides/operations/cli):
22+
`DashboardServer.start(queue, port)` opens in [session-auth
23+
mode](#session-auth-default). `start(queue, port, token)` switches to
24+
[legacy shared-token mode](#legacy-shared-token-mode); four- and five-argument
25+
overloads add an unpacked SPA directory and a `secureCookies` flag.
26+
27+
</Tab>
28+
<Tab value="Taskito.dashboard()">
29+
30+
```java
31+
try (Taskito taskito = Taskito.builder().sqlite("taskito.db").open();
32+
DashboardServer server = taskito.dashboard(8080)) {
33+
// ...
34+
}
35+
36+
// Legacy shared-token mode, gating /api/* as a fixed admin identity:
37+
taskito.dashboard(8080, System.getenv("DASH_TOKEN"));
38+
```
39+
40+
`Taskito.dashboard(port)` / `dashboard(port, token)` are convenience defaults
41+
over `DashboardServer.start(...)` for the common case — one fewer import.
42+
43+
</Tab>
44+
<Tab value="CLI">
1845

1946
```bash
2047
taskito --url taskito.db dashboard --port 8080
2148
```
2249

50+
| Flag | Default | Description |
51+
|---|---|---|
52+
| `--port` | `8080` | Bind port (`0` for ephemeral) |
53+
| `--token` | none | Legacy shared token gating `/api/*` — disables session auth and OAuth |
54+
| `--static` | bundled SPA | Directory of a prebuilt SPA, overriding the jar's extracted copy |
55+
| `--insecure-cookies` | off | Drop the `Secure` cookie attribute — for local HTTP development |
56+
57+
</Tab>
58+
</Tabs>
59+
2360
Pass `0` as the port for an ephemeral one (`server.port()` reports what was
2461
bound). The SPA is extracted from the jar to a per-user, content-addressed
2562
directory on first use; `-Dtaskito.dashboard.dir=/path` (or the `staticDir`
26-
argument) overrides it with an unpacked build. Without bundled assets only
27-
`/api/*` responds.
63+
argument / `--static`) overrides it with an unpacked build. Without bundled
64+
assets only `/api/*` responds.
2865

29-
## REST API
66+
<Callout type="info">
67+
Running Spring Boot? `taskito-spring` can auto-start a `DashboardServer` bean
68+
from `taskito.dashboard.*` properties — see
69+
[Spring Boot: Dashboard auto-configuration](/java/guides/integrations/spring#dashboard-auto-configuration).
70+
</Callout>
71+
72+
## Auth
73+
74+
Two modes, chosen by whether a `token` is passed to `start(...)` /
75+
`dashboard(...)`.
3076

31-
Everything is JSON, fields in snake_case, timestamps in Unix milliseconds.
77+
### Session auth (default)
3278

33-
| Method · Path | Effect |
79+
With no token, the dashboard runs password sign-in (and optionally
80+
[OAuth/OIDC](/java/guides/operations/sso)) with server-side sessions. Users
81+
and sessions live in the queue's settings key/value store — no dedicated
82+
tables — so the model is identical across SQLite, PostgreSQL, and Redis.
83+
84+
- **First-run setup.** On a fresh database every route except the public set
85+
(`/api/auth/status`, `/api/auth/login`, `/api/auth/setup`,
86+
`/api/auth/providers`, `/health`, `/readiness`, `/metrics`) returns
87+
`503 setup_required` until an admin exists. `POST /api/auth/setup` creates
88+
it (and signs it in); the route locks itself after the first user.
89+
- **Env-admin bootstrap.** Set both `TASKITO_DASHBOARD_ADMIN_USER` and
90+
`TASKITO_DASHBOARD_ADMIN_PASSWORD` before starting the process to seed the
91+
first admin without visiting a browser — useful for containers. It's
92+
idempotent: once a user with that name exists, later restarts skip
93+
creation.
94+
95+
```bash
96+
export TASKITO_DASHBOARD_ADMIN_USER=admin
97+
export TASKITO_DASHBOARD_ADMIN_PASSWORD='change-me-on-first-login'
98+
taskito --url taskito.db dashboard --port 8080
99+
```
100+
101+
<Callout type="warning">
102+
Unlike a scripting-language runtime, the JVM cannot scrub a variable out
103+
of its own process environment once it has been read — the password
104+
stays visible to anything that can inspect the process (`/proc`, a
105+
debugger, a core dump) for the process's lifetime. Prefer first-run setup
106+
through the SPA where that matters; treat the env var as a one-time
107+
recovery path and rotate the password after logging in.
108+
</Callout>
109+
110+
- **Passwords** are hashed with PBKDF2-HMAC-SHA256 — 600,000 iterations, a
111+
16-byte random salt — no third-party crypto dependency.
112+
- **Sessions** are opaque tokens with a 24-hour TTL, carried in an `HttpOnly`,
113+
`SameSite=Strict` `taskito_session` cookie (plus `Secure` unless disabled —
114+
see below).
115+
- **CSRF** uses the double-submit pattern: a non-HttpOnly `taskito_csrf`
116+
cookie must match both the token bound to the session and the
117+
`X-CSRF-Token` header on every state-changing request
118+
(`POST`/`PUT`/`DELETE`/`PATCH`). `/api/auth/login` and `/api/auth/setup` are
119+
exempt — there is no session yet to bind to.
120+
- **`--insecure-cookies`** (or `secureCookies=false` on `DashboardServer.start`,
121+
or `taskito.dashboard.secure-cookies=false` in Spring) drops the `Secure`
122+
cookie attribute for local HTTP development. Keep it on — the default — for
123+
anything served over HTTPS.
124+
125+
### Roles
126+
127+
RBAC is enforced server-side and is deliberately simple: every state-changing
128+
route is admin-only except two self-service routes; all reads are open to any
129+
authenticated user.
130+
131+
| Role | Access |
34132
|---|---|
35-
| `GET /api/stats` | Counts by status across all queues. |
36-
| `GET /api/stats/queues` | Counts per queue. |
37-
| `GET /api/queues/paused` | Names of paused queues. |
38-
| `GET /api/jobs` | Job list — `?status=&queue=&task=&limit=&offset=`. |
39-
| `GET /api/jobs/{id}` | A single job. |
40-
| `GET /api/dead-letters` | Dead-letter entries — `?limit=&offset=`. |
41-
| `GET /api/metrics` | Per-execution metrics — `?task=&since=` (ms window). |
42-
| `GET /api/workers` | Registered workers + heartbeats. |
43-
| `GET /api/auth/status` | `{ "auth_required": true\|false }` — never needs a token. |
44-
| `POST /api/jobs/{id}/cancel` | Cancel a job. |
45-
| `POST /api/dead-letters/{id}/retry` | Re-enqueue a dead-letter entry. |
46-
| `POST /api/queues/{name}/pause` · `/resume` | Pause / resume a queue. |
133+
| `admin` | Full access — cancel/replay jobs, purge dead letters, pause/resume queues, manage webhooks, edit settings, edit task/queue overrides. |
134+
| `viewer` | Read-only, plus their own `POST /api/auth/logout` and `POST /api/auth/change-password`. Any other mutating route returns `403 forbidden`. |
47135

48-
## Auth
136+
The first user — created via setup or env bootstrap — is always `admin`.
137+
138+
### Legacy shared-token mode
49139

50-
Auth runs **open** by default. Pass a token to require it on every `/api/*`
51-
request (except `/api/auth/status`):
140+
Pass a `token` to gate `/api/*` behind a single fixed credential — no users,
141+
no sessions, no RBAC. Kept for back-compat with the pre-auth dashboard.
52142

53143
```java
54144
DashboardServer.start(taskito, 8080, System.getenv("DASH_TOKEN"));
55145
```
56146

57147
Requests authenticate with `?token=<token>`; opening `/?token=<token>` once
58148
sets an httpOnly `taskito_token` cookie so the SPA works for the rest of the
59-
session. This is a single shared token — no per-user login, RBAC, or SSO. For
60-
those, put the server behind a reverse proxy that handles auth.
149+
session. OAuth has no login UI in this mode, so it's disabled automatically —
150+
`start(queue, port, token, ...)` never builds an OAuth flow when `token` is
151+
non-null.
61152

62153
<Callout type="warning">
63154
`?token=` puts the secret in the URL, where it can leak via browser history,
64155
`Referer` headers, and proxy or access logs. Use it only over HTTPS, redact
65-
query strings from logs, and rely on the cookie afterwards — once the first
66-
request sets it, the token never needs to appear in a URL again.
156+
query strings from logs, and rely on the cookie afterwards.
67157
</Callout>
68158

159+
## Metrics and health probes
160+
161+
Three routes sit outside `/api/*` and outside the session/token auth gate:
162+
163+
| Route | Access | What it does |
164+
|---|---|---|
165+
| `GET /health` | Always public | Liveness — always `{"status": "ok"}` |
166+
| `GET /readiness` | Public unless `TASKITO_DASHBOARD_METRICS_TOKEN` is set | Storage/worker/resource readiness |
167+
| `GET /metrics` | Public unless `TASKITO_DASHBOARD_METRICS_TOKEN` is set | Prometheus text exposition |
168+
169+
Set `TASKITO_DASHBOARD_METRICS_TOKEN` to require an
170+
`Authorization: Bearer <token>` header (checked in constant time) on
171+
`/readiness` and `/metrics`. `/health` always stays open for liveness probes.
172+
173+
## REST API
174+
175+
Everything is JSON, fields in snake_case, timestamps in Unix milliseconds —
176+
the same contract the bundled SPA consumes. All paths below are relative to
177+
`/api/`.
178+
179+
| Group | Routes |
180+
|---|---|
181+
| Auth | `auth/status`, `/setup`, `/login`, `/logout`, `/whoami`, `/change-password`, `/providers`, `/oauth/start/{slot}`, `/oauth/callback/{slot}` — see [SSO](/java/guides/operations/sso) |
182+
| Stats & jobs | `stats`, `stats/queues`, `queues/paused`, `jobs` (+ `/{id}`, `/{id}/logs`, `/{id}/replay-history`, `/{id}/dag`, `/{id}/cancel`, `/{id}/replay`) |
183+
| Dead letters | `dead-letters` (+ `/{id}/retry`) |
184+
| Metrics & logs | `metrics`, `metrics/timeseries`, `logs` |
185+
| Infrastructure | `workers`, `circuit-breakers`, `resources`, `scaler`, `event-types` |
186+
| Queue control | `queues/{name}/pause`, `queues/{name}/resume` |
187+
| Task/queue overrides | `tasks`, `tasks/{name}/override`, `queues`, `queues/{name}/override` — runtime rate limit, concurrency, retries, timeout, priority, and pause, without redeploying |
188+
| Webhooks | `webhooks` (+ `/{id}`, `/{id}/test`, `/{id}/rotate-secret`, `/{id}/deliveries`, `/{id}/deliveries/{deliveryId}`, `/{id}/deliveries/{deliveryId}/replay`) — see [Webhooks: Dashboard management](/java/guides/extensibility/webhooks#dashboard-management) |
189+
| Workflows | `workflows/runs` (+ `/{id}`, `/{id}/dag`, `/{id}/children`) |
190+
| Settings | `settings`, `settings/{key}` |
191+
69192
<Callout type="warning">
70-
Open mode means anyone who can reach the port has full control. Bind it to
71-
localhost, front it with your own auth, or at minimum set a token — see
72-
[Security](/java/guides/operations/security).
193+
Every route above is auth-gated: session mode requires a valid session (plus
194+
CSRF on writes) except the public auth routes; legacy mode requires the
195+
matching token. See [Auth](#auth).
73196
</Callout>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "title": "Operations", "pages": ["backends", "inspection", "dashboard", "mesh", "autoscaling", "cli", "testing", "security", "troubleshooting", "deployment", "graalvm"] }
1+
{ "title": "Operations", "pages": ["backends", "inspection", "dashboard", "sso", "mesh", "autoscaling", "cli", "testing", "security", "troubleshooting", "deployment", "graalvm"] }

docs/content/docs/java/guides/operations/security.mdx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ secret; the signature rides in `X-Taskito-Signature: sha256=<hex>`. Verify it
3636
on the receiver — HMAC the raw body with the shared secret and compare in
3737
constant time — before trusting the body.
3838

39-
The deliverer POSTs the configured URL directly — there is **no SSRF
40-
allowlist.** Only register webhook URLs you control, and validate them before
41-
storing if they come from users.
39+
Dashboard-submitted webhook URLs are checked by an SSRF guard (loopback,
40+
link-local, RFC1918, multicast, CGNAT, and IPv6 unique-local addresses are
41+
rejected by default), and every delivery re-validates the URL again right
42+
before sending, closing the DNS-rebinding gap. `WebhookManager.create(...)`
43+
called directly from your own code is trusted input and isn't pre-validated
44+
at creation — only at delivery time. Set `TASKITO_WEBHOOKS_ALLOW_PRIVATE` to
45+
disable the guard for local development. See
46+
[Webhooks: SSRF guard](/java/guides/extensibility/webhooks#ssrf-guard).
4247

4348
## Proxies
4449

@@ -50,10 +55,15 @@ permits any path**, so always set roots in production.
5055

5156
## Dashboard
5257

53-
The [dashboard](/java/guides/operations/dashboard) runs in **open mode** by
54-
default — anyone who reaches the port has full control. Start it with a token
55-
to gate `/api/*`, bind it to localhost, or front it with a reverse proxy that
56-
enforces your own auth (login / RBAC / SSO).
58+
The [dashboard](/java/guides/operations/dashboard) requires sign-in by
59+
default: on a fresh database every route except a small public set returns
60+
`503 setup_required` until an admin exists, and every route after that needs
61+
a valid session (admin/viewer RBAC, CSRF on writes) or, if configured,
62+
[OAuth/OIDC](/java/guides/operations/sso). Passing a `token` instead switches
63+
to legacy shared-token mode — no users, no sessions, no RBAC — kept for
64+
back-compat; anyone with the token has full control. Either way, bind the
65+
port to a trusted network or front it with a reverse proxy for
66+
defense in depth.
5767

5868
## Mesh gossip
5969

@@ -77,6 +87,7 @@ hosts with a `noexec` `/tmp`.
7787
- [ ] Payloads signed or encrypted where the storage host isn't fully trusted.
7888
- [ ] Webhook receivers verify the HMAC signature; webhook URLs are trusted.
7989
- [ ] `FileProxyHandler` configured with allowlisted roots.
80-
- [ ] Dashboard bound to localhost, or token-gated behind your own auth.
90+
- [ ] Dashboard admin account created (or env-bootstrapped) on first deploy;
91+
bound to a trusted network regardless of auth mode.
8192
- [ ] Logs don't echo sensitive payloads (redact in `onEnqueue`
8293
[middleware](/java/guides/extensibility/middleware)).

0 commit comments

Comments
 (0)