mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 08:02:53 +00:00
* refactor: make the app policy's principal the authority for its identity * fix: align the app backfill with the sibling migration and audit the uncached address * chore: refresh the sqlx cache after rebasing onto the merged base * fix: resolve the app execution address uncached, it decides the job's authorization * chore: cache the EE queries at the ref this branch pins * chore: cache the EE queries at the ref this branch pins * fix: derive the app draft's on-behalf-of address on read * chore: cache the query the draft derivation test added * fix: derive the app identity on the draft-table and version reads too * docs: state the draft resolver's authorization contract * fix: resolve a draft's principal against workspace membership only * chore: cache the membership lookup the draft resolver added * fix: drop an unresolvable draft's address instead of leaving it stale * perf: evict the address cache on change so app dispatch can read it * fix: evict on superadmin role changes, not only address changes * refactor: make the app policy's address optional instead of derived on read * fix: follow an external superadmin's rename into the apps that name them * docs: state the removal gate once, and correctly * refactor: drop the app-policy version constant that gated nothing * docs: drop the last reference to the removed constant * perf: read the address cache everywhere now that eviction reaches every replica * fix: keep persisted addresses off the cache the poller evicts asynchronously * docs: state where the cached address is accepted and where it is not * docs: keep the cache rule in one place and drop the stale premise * docs: sort the two lookups by how long a wrong answer lives * fix: resolve the schedule address uncached where it is written to the row * docs: name the release this actually ships in * perf: evict a superadmin's key per workspace instead of the whole cache * fix: evict every alias a superadmin principal can be spelled as * docs: describe the trigger as it is * docs: cover the round-tripped read in the cache rule * docs: record why a stale dispatch address cannot escalate * fix: validate a dispatch address against the principal's live binding * fix: carry the validated address through to the job row and token * fix: record the validated address on the job row, not the one handed in * test: run the substep tag check as the non-superadmin it means to test Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: rewrite a stored app address that disagrees with its principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: record the accepted staleness window of the cached dispatch address Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: record the validated address on the job's audit row Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: record the accepted rename race of pre-transaction identity resolution Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: separate the app's stored address from the derived one in the resolver doc Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: describe the job identity fast path the push comments skipped Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: backfill a legacy group-prefixed username as the group it names Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: resolve a schedule edit's identity before opening its transaction Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: never resolve a disabled member to a same-named superadmin Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: state what the email-change notify buys, and rewrap two comment lines Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: keep a group's runnables when offboarding a legacy group-prefixed member Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * fix: read the app author from the stored address, as execution does Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: record the rename race's full consequence as a known, accepted limitation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc * docs: record the keep-target group address case as a known, accepted limitation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
82 lines
3.5 KiB
Rust
82 lines
3.5 KiB
Rust
use sqlx::{Pool, Postgres};
|
|
use windmill_common::auth::fetch_authed_from_permissioned_as;
|
|
|
|
/// The address handed to `fetch_authed_from_permissioned_as` may come from a cache that a
|
|
/// username reassignment has outrun. It must not be believed: the workspace role is keyed on the
|
|
/// principal while `super_admin` and `email_to_igroup` are keyed on the address, so trusting a
|
|
/// stale one would run the new holder's job with the previous holder's instance privileges.
|
|
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
|
async fn test_stale_address_cannot_carry_the_previous_holders_privileges(db: Pool<Postgres>) {
|
|
// `test-user` in the fixture is a superadmin with the address `test@windmill.dev`. Free the
|
|
// username and hand it to somebody who is not, exactly as an offboard-then-onboard would.
|
|
sqlx::query("DELETE FROM usr WHERE workspace_id = 'test-workspace' AND username = 'test-user'")
|
|
.execute(&db)
|
|
.await
|
|
.expect("free the username");
|
|
sqlx::query(
|
|
"INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
|
|
VALUES ('newcomer@windmill.dev', 'x', 'password', false, true, 'Newcomer')",
|
|
)
|
|
.execute(&db)
|
|
.await
|
|
.expect("create the new account");
|
|
sqlx::query(
|
|
"INSERT INTO usr(workspace_id, email, username, is_admin, role)
|
|
VALUES ('test-workspace', 'newcomer@windmill.dev', 'test-user', false, 'User')",
|
|
)
|
|
.execute(&db)
|
|
.await
|
|
.expect("reassign the username");
|
|
|
|
// What a replica that has not yet consumed the eviction would pass: the principal is the
|
|
// reassigned username, the address is the one it cached for the previous holder.
|
|
let authed = fetch_authed_from_permissioned_as(
|
|
"u/test-user",
|
|
"test@windmill.dev",
|
|
"test-workspace",
|
|
&db,
|
|
)
|
|
.await
|
|
.expect("should authenticate the current holder");
|
|
|
|
assert_eq!(
|
|
authed.email, "newcomer@windmill.dev",
|
|
"the principal's live address must win over the one supplied"
|
|
);
|
|
assert!(
|
|
!authed.is_admin,
|
|
"the new holder must not inherit the previous holder's superadmin"
|
|
);
|
|
}
|
|
|
|
/// A disabled member still holds its username in the workspace. Workspace usernames are only
|
|
/// unique per workspace, so an unrelated instance superadmin can share it, and falling through to
|
|
/// the `password` fallback would run the disabled member's jobs as that superadmin.
|
|
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
|
async fn test_disabled_member_never_resolves_to_a_same_named_superadmin(db: Pool<Postgres>) {
|
|
sqlx::query(
|
|
"UPDATE usr SET disabled = true WHERE workspace_id = 'test-workspace' AND username = 'test-user-2'",
|
|
)
|
|
.execute(&db)
|
|
.await
|
|
.expect("disable the member");
|
|
sqlx::query(
|
|
"INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
|
|
VALUES ('other-superadmin@windmill.dev', 'x', 'password', true, true, 'Other', 'test-user-2')",
|
|
)
|
|
.execute(&db)
|
|
.await
|
|
.expect("create the same-named superadmin");
|
|
|
|
for supplied in ["test2@windmill.dev", "other-superadmin@windmill.dev"] {
|
|
let authed =
|
|
fetch_authed_from_permissioned_as("u/test-user-2", supplied, "test-workspace", &db)
|
|
.await;
|
|
assert!(
|
|
authed.is_err(),
|
|
"a disabled member must not authenticate (supplied {supplied}): {:?}",
|
|
authed.map(|a| (a.email, a.is_admin))
|
|
);
|
|
}
|
|
}
|