mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-16 16:03:14 +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>
86 lines
4.1 KiB
PL/PgSQL
86 lines
4.1 KiB
PL/PgSQL
-- Add up migration script here
|
|
-- Emit a notify_event so every process evicts its cached `permissioned_as` -> address mapping
|
|
-- (windmill-common EMAIL_CACHE) at its next notify-event poll, rather than serving the old
|
|
-- address for the rest of the TTL. Authorization does not rest on this:
|
|
-- `fetch_authed_from_permissioned_as` re-resolves the address from the principal's live binding.
|
|
-- SECURITY DEFINER so the INSERT runs as the function owner rather than the invoking
|
|
-- windmill_user/windmill_admin role, matching the other notify_* triggers.
|
|
CREATE OR REPLACE FUNCTION notify_usr_email_change()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO notify_event (channel, payload)
|
|
VALUES (
|
|
'notify_user_email_change',
|
|
COALESCE(NEW.workspace_id, OLD.workspace_id) || ':' || COALESCE(NEW.username, OLD.username)
|
|
);
|
|
-- A rename leaves the OLD username cached against this account's address; evict both keys.
|
|
IF TG_OP = 'UPDATE' AND NEW.username IS DISTINCT FROM OLD.username THEN
|
|
INSERT INTO notify_event (channel, payload)
|
|
VALUES ('notify_user_email_change', OLD.workspace_id || ':' || OLD.username);
|
|
END IF;
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- INSERT matters too: a lookup that resolved to nobody is cached as the synthetic
|
|
-- `{username}@unknown.windmill.dev`, so creating the row has to drop that entry.
|
|
CREATE TRIGGER usr_email_change_trigger
|
|
AFTER INSERT OR DELETE ON usr
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION notify_usr_email_change();
|
|
|
|
CREATE TRIGGER usr_email_update_trigger
|
|
AFTER UPDATE OF email, username ON usr
|
|
FOR EACH ROW
|
|
WHEN (OLD.email IS DISTINCT FROM NEW.email OR OLD.username IS DISTINCT FROM NEW.username)
|
|
EXECUTE FUNCTION notify_usr_email_change();
|
|
|
|
-- A superadmin acting outside their workspaces resolves through `password` instead, and that row
|
|
-- names no workspace of its own. The `*:` payload says so: the reader drops that name's entry in
|
|
-- every workspace rather than the whole cache, which would undo the caching on an instance that
|
|
-- rewrites these rows in bulk. Confined to superadmins because they are the only accounts the
|
|
-- `usr` triggers above cannot cover.
|
|
CREATE OR REPLACE FUNCTION notify_superadmin_identity_change()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
names TEXT[] := '{}';
|
|
BEGIN
|
|
-- Every alias the principal can be spelled as: `resolve_username_to_email` matches a `u/`
|
|
-- principal against `username` OR `email`, and whichever string the caller passed is the key
|
|
-- it cached under, so one account can hold a live entry under either. Old and new of each,
|
|
-- because a change to one leaves the other's entry behind.
|
|
IF TG_OP <> 'DELETE' THEN names := names || ARRAY[NEW.username, NEW.email]; END IF;
|
|
IF TG_OP <> 'INSERT' THEN names := names || ARRAY[OLD.username, OLD.email]; END IF;
|
|
INSERT INTO notify_event (channel, payload)
|
|
SELECT DISTINCT 'notify_user_email_change', '*:' || n
|
|
FROM unnest(names) AS n
|
|
WHERE n IS NOT NULL;
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- `super_admin` is half of what the fallback matches on, so gaining or losing it moves the
|
|
-- mapping as surely as the address does: a demotion leaves the real address cached where the
|
|
-- truth is now synthetic, and a promotion leaves that synthetic one cached in place of a real
|
|
-- account. `OLD.super_admin OR NEW.super_admin` is what catches both directions.
|
|
CREATE TRIGGER password_superadmin_update_trigger
|
|
AFTER UPDATE OF email, username, super_admin ON password
|
|
FOR EACH ROW
|
|
WHEN ((OLD.super_admin OR NEW.super_admin)
|
|
AND (OLD.email IS DISTINCT FROM NEW.email
|
|
OR OLD.username IS DISTINCT FROM NEW.username
|
|
OR OLD.super_admin IS DISTINCT FROM NEW.super_admin))
|
|
EXECUTE FUNCTION notify_superadmin_identity_change();
|
|
|
|
CREATE TRIGGER password_superadmin_insert_trigger
|
|
AFTER INSERT ON password
|
|
FOR EACH ROW
|
|
WHEN (NEW.super_admin)
|
|
EXECUTE FUNCTION notify_superadmin_identity_change();
|
|
|
|
CREATE TRIGGER password_superadmin_delete_trigger
|
|
AFTER DELETE ON password
|
|
FOR EACH ROW
|
|
WHEN (OLD.super_admin)
|
|
EXECUTE FUNCTION notify_superadmin_identity_change();
|