mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 16:02:57 +00:00
datatable-external-instance
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
45102c8265 |
fix: let the hub_sync job read the uid and hub_base_url settings (#11106)
Claude-Session: https://claude.ai/code/session_01Q6triDksvGJ4YK2gA1acEc Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8aab5034a6 |
feat: guest JWT entry for embedded apps (#10954)
* feat: guest JWT entry for embedded apps (jwt_guest_) A second way in for a guest, alongside the signed-in guest session: a JWT the embedding customer's backend mints and signs, verified per request against a per-workspace key (a PEM public key or a JWKS URL), resolving to the same seatless guest identity confined to the one app its app_path claim names. Bearer prefix jwt_guest_, stateless (no token row). See PR #10954. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: surface guest JWT as the embed method in the app deploy drawer The deploy drawer explained the secret-URL embed but not the guest JWT path, so the primary way to embed an app for a customer's own authenticated users was undiscoverable. For a guest-mode app with guests enabled, show how to mint a `jwt_guest_` token and append `guest.<jwt>` to the app URL, with a copyable iframe template pre-filled with this app's workspace_id and app_path, and a note that new guest emails are refused past the instance's free allowance (the live count is shown just above). Also log a guest JWT allowance refusal at warn, not info: the caller gets a bare 401 (the reason must not leak to an unauthenticated caller), so the log is the admin's signal that the instance hit its guest cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: correct the guest JWT minting instructions in the embed block The block said "sign it with the workspace's guest JWT key", but that setting holds the public verification key. Clarify the keypair relationship (configure the public key or a JWKS URL in the workspace; sign with the matching private key), name the accepted algorithms (RS/PS/ES; HS* refused), and keep the required claims, so an embedder knows how to actually mint the token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: fall back to the instance JWT issuer for guest verification (off on cloud) A workspace with no guest key of its own now verifies guest JWTs against the instance issuer (JWT_EXT_JWKS_URL, already used by jwt_ext_), so an operator running one issuer configures it once. Verification and the guest grant are CE; granting a full login from that issuer stays EE (jwt_ext_, unchanged). Disabled under CLOUD_HOSTED, where one instance issuer must not be trusted to mint guests in every tenant's workspace — there the per-workspace key is the only source, which also stays the override everywhere. The workspace settings note (hidden on cloud) explains the fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: embed instructions cover both the workspace key and instance issuer The embed block said to set the workspace's guest JWT key; now it says Windmill verifies against the workspace key or, off cloud, the instance issuer (JWT_EXT_JWKS_URL) when no workspace key is set. The instance clause is hidden under isCloudHosted(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: show the guest JWT embed block only when Embed is toggled It belongs with the iframe snippet, not the plain-URL view, so gate it on embedMode alongside the guest-mode / guests-enabled checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: trust the instance issuer in the guest fallback; refresh stale docs P1 (CI review): the fallback wrapped JWT_EXT_JWKS_URL as a workspace JwksUrl, so it hit validate_guest_jwks_url and was refused for http/private issuers unless ALLOW_PRIVATE_GUEST_JWKS_URLS was also set — a self-hosted internal issuer that works for jwt_ext_ failed for guests, though the UI says setting the env var is enough. fetch_jwks now fetches the instance issuer without the https/private restriction (matching the jwt_ext_ loader; it stays operator-trusted), while a workspace-admin URL is validated and pinned as before. All the size/key/URL bounds still apply to both. P2 (CI review): refresh the stale docs that said a missing workspace key always refuses a guest JWT — the module, bearer, key-source, and EditGuestJwtKey field docs now describe the workspace key with the off-cloud instance-issuer fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: fetch the trusted instance issuer like the jwt_ext_ loader P1 (CI review): the instance-issuer fetch skipped SSRF validation but still disabled redirects and default cert validation, so an instance issuer that works for jwt_ext_ through a redirect or an operator-approved self-signed cert failed the guest fallback. Fetch it with HTTP_CLIENT_PERMISSIVE (follows redirects, honors ACCEPT_INVALID_CERTS) — the same behavior jwt_ext_ has — while a workspace-admin URL stays validated, DNS-pinned and redirect-free. The body size cap still bounds both. P2 (CI review): the WorkspaceSettings field doc still said None/None means no JWT guests; it now names the off-cloud instance-issuer fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: schema summary + OpenAPI cover the guest JWT columns and fallback P2 (CI review): summarized_schema.txt was missing guest_activity.jwt_entry and the two workspace_settings guest-JWT key columns (required by docs/validation.md after a schema change). The edit_guest_jwt_key OpenAPI description now notes that clearing the workspace key falls back to the instance issuer (JWT_EXT_JWKS_URL) off cloud rather than necessarily stopping guest JWTs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: keep JWKS single-flight locks in a self-cleaning map, not a bounded cache P1 (CI review): JWKS_FETCH_LOCKS was a 200-entry quick_cache. Past 200 cold URLs it can evict a lock whose fetch is still in flight; the next request for that URL then mints a fresh lock and starts a second fetch, so cycling configured workspaces defeats single-flight and can storm the issuers. Replace it with a plain map guarded by a JwksFetchLock RAII handle that removes each entry once its last holder drops, so the map only ever holds the fetches in flight and never evicts an in-flight lock. Add a unit test pinning the shared-lock and self-cleaning invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: update ee-repo-ref to c2270eb5fe2d9f0968253e6b460c33186363f4e7 This commit updates the EE repository reference after PR #773 was merged in windmill-ee-private. Previous ee-repo-ref: 5a1d9dee34159512c0823fddcd3d096490edbcce New ee-repo-ref: c2270eb5fe2d9f0968253e6b460c33186363f4e7 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
c2279db8a9 |
fix: allow job tokens to read the automate_username_creation setting (#10869)
* fix: let a job token read the automate_username_creation setting Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: use an ungated global setting as the confinement control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9022dc9d44 |
fix: confine job tokens to workspace-scoped API routes (#10631)
* fix: confine job tokens to workspace-scoped API routes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the object-storage connection test reachable from a job token Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the workspace-exists check the CLI makes reachable from a job Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: reconcile the job-token caps after #10124 The workspace-confinement middleware answers a workspace-less route before the privilege gate behind it runs, so the cases #10124 added on those routes now see 403 rather than 401. Rejection is what they assert, but two of them needed more than a status change: - `list_worker_groups` asserted only that the response body omits the static env value, which an error body satisfies for the wrong reason. It now asserts the status, keeping the secret check as a second assertion. - `require_super_admin` lost its only unshadowed route. `GET /api/w/{workspace}/users/list_addable` is gated solely by that call and names a workspace, so it reaches the gate and pins it at 401. The module doc states the two-layer rule once; the file covers both caps, so it is no longer named for either one alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: correct the workspaces/exists rationale and the parquet gate note `workspace` carries no row-level security, so `exists_workspace` running through `user_db` does not filter by membership as the comment claimed. State what the route actually discloses — whether a workspace id is taken. The object-storage case explained why a 404 would satisfy the assertion for the wrong reason, which described the earlier `assert_ne!(403)`; against the 422 it now asserts, a 404 fails. Say instead why the case is gated on the feature. * fix: let a job token keep the workspace-less routes that carry no workspace Confinement refused every route outside the allowlist, including ones that answer purely from the caller's own account or from the request body. Those cross no workspace boundary, so refusing them buys nothing: - `users/email` returns a value already inside the token, and `workspaces/allowed_domain_auto_invite` tests the caller's own address against a static list. Neither opens a transaction. - `users/usage` reads the caller's own row; `users/tutorial_progress` reads and upserts a UI bitfield keyed on the same email. - `schedules/preview` takes no `ApiAuthed` at all — it computes the occurrences of the cron expression in the body and returns nothing the caller did not send. The rule, not the list, is what the doc comment states: answers from the caller's own account, the request body, or content identical for every workspace; never naming another workspace, never instance configuration. The candidates it excludes are written down with their reasons, since `users/list_invites` reads as caller-scoped until you notice the response carries a workspace id per invite. Regression covers both directions — the new entries answer, and the rejected caller-scoped reads stay refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: reach the privilege gates confinement hides `require_devops_role` and `require_instance_admin` gate only workspace-less routes, so confinement answers every request that would reach them: no HTTP case can tell whether they still cap job tokens, and both could lose that check with this suite green. `require_super_admin` has a workspace-scoped route to reach it; these two have none, so call them directly instead. The identity used is the fixture's real superadmin, so the passing half proves the rejection keys off `job_id` rather than off the user. Also separate two claims the write-allowlist doc had merged into one sentence: no entry writes outside the caller's own account, but what each may read differs, and the workspace-existence check answers for any id. * docs: say that the object-storage probe writes The write-allowlist lead claimed no entry writes state outside the caller's own account. `test_s3_bucket` puts an object into the store the body names and deletes it again, so it does write; a failure between the two leaves the object behind. The invariant that holds is about Windmill state. Say so in the lead, and describe the put/delete in the entry itself rather than leaving "acts only on the store the request body describes" to imply a read. * test: cover the last two job-token gates confinement hides Seven guards key on `ApiAuthed::job_id`. Three keep a workspace-scoped route and are exercised over HTTP; the other four are reachable only through workspace-less routes, which confinement now answers first, so nothing observed whether they still cap job tokens. `require_devops_role` and `require_instance_admin` were already called directly. Add the two that were not: `forbid_superadmin_job_token`, and `forbid_elevated_job_token`, whose call sites are `create_token`, `update_token_scopes` and `set_password` — all workspace-less. Both key on two conditions rather than one, so all three combinations are pinned: neither fires without job provenance, and neither fires for an unelevated identity. The second matters — collapsing either into a blanket job-token refusal would stop ordinary users creating tokens, and no other case would catch it. The doc comment records which of the seven each route covers. * docs: correct which job-token gates have no observable route The previous commit put `forbid_elevated_job_token` among the guards reachable only through workspace-less routes, and its message named three call sites. It has six, and two are workspaced: `mint_app_embed_token` and `mint_raw_app_sdk_token`. Its superadmin branch is therefore already exercised over HTTP — the 401 the embed-token case asserts is this gate. So three of the seven lack an observable route, not four. Its direct assertions stay: the embed-token case only ever reaches it with an elevated identity, and the unelevated-negative case is what would catch the gate being collapsed into a blanket job-token refusal. * test: pin is_instance_admin, and stop enumerating gates in prose `is_instance_admin` is `authed.is_admin && authed.job_id.is_none()`, so a census built by searching for `job_id.is_some()` could not see it. Both its call sites are workspace-less, and it returns a bool that selects obfuscation rather than refusing — a job token reading `true` leaks `env_vars_static` instead of being turned away. Pin both directions. The doc comment tried to account for every job-token guard and which route exercised it. It was wrong three times running: the count, the call sites of `forbid_elevated_job_token`, and the claim that the CUSTOM_INSTANCE_DB case covers `is_super_admin_authed` when that path tests `job_id` inline. A table that has to be rederived from six crates to stay true does not belong in a comment, so it now states only why these calls are direct. * docs: name the right is_instance_admin caller The comment credited "the concurrency-group listing" with obfuscating rows. The second caller is `prune_concurrency_group`, which returns PermissionDenied; the obfuscating one is `list_worker_groups`. Keep the claim to that single caller, which is what makes this guard fail by leaking `env_vars_static` rather than by admitting a request. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |