mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
* feat: make the fork lineage the only deploy relationship `workspace_settings.deploy_to` (2023) and `workspace.parent_workspace_id` (2025) both expressed "which workspace does this one deploy into". Fork creation and dev-workspace attach seeded both, but nothing kept them in agreement, so every reader picked one and they disagreed. Drop `deploy_to`. A migration folds surviving pairs into the lineage: a sole claimant on a target with no dev workspace becomes that target's dev workspace and keeps its own job tags, while many-to-one pairs become plain forks. Pairs that the lineage cannot express -- dangling target, self-reference, chain, mutual -- are reported and left unlinked. Job tags were never lineage-aware: `per_workspace_tag` mapped any parented workspace to its parent while `$workspace` interpolated the raw id, so a fork running a script tagged `<tag>-$workspace` produced a tag no worker serves and the job queued forever. Both paths now resolve to the nearest ancestor whose id an admin would provision workers for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: preserve unconvertible deploy links and sweep tag caches on reparent Review findings on the deploy_to unification: - convert chains instead of discarding them, and keep whatever the lineage cannot express in workspace_deploy_to_unmigrated so the down migration can restore it - ignore soft-deleted workspaces when choosing between a dev workspace and a plain fork; an archived claimant was demoting live pairs - mirror attach_dev_workspace's git-sync strip, which the migration skipped - sweep the tag cache over whole subtrees on rename and delete: tag resolution now walks ancestors, so a nested fork kept a tag nothing serves - call a dev workspace a dev workspace in the settings copy - redirect a root away from ?tab=deploy_to instead of rendering an empty target Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: detect lineage cycles and record archived links in the deploy_to migration Second review round on the unification: - detect cycles over the lineage as it would exist after conversion, not over the deploy_to graph alone: a root whose target was one of its own forks closed a loop that no deploy_to edge revealed - record an archived source's link instead of filtering it out entirely, which dropped it with the column - treat a fork whose deploy_to merely repeats its parent as redundant rather than reporting every pre-existing fork as unmigrated - read the row count from the lineage update rather than the git-sync one - sweep the tag cache when archiving a dev workspace, the last site that mutates is_dev_workspace without one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: resolve $workspace on preprocessed flow tags regardless of $args Third review round on the unification: - a flow tag containing only `$workspace` skipped interpolation entirely on the preprocessed path, because the branch that ran it keys on `$args`. The raw tag was written back and named a queue no worker serves. Resolve `$workspace` before the branch and leave `$args` to it. - record the new table's foreign key in the schema summary - describe what the archive tag sweep actually does: the dev flag is cleared for any archived workspace, which is why it is unconditional Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the deploy_to leftovers table only when it holds something * fix: sweep tag caches on archive only where the dev flag actually changes * feat: broadcast lineage changes and walk ws_specific ancestors only - propagate tag-cache invalidation across processes over notify_events: the cache is per-process, so replicas kept resolving stale lineage for the TTL. The listener clears the whole cache rather than tracking ids, since a single mutation invalidates an unbounded set of descendants and lineage changes are rare admin actions. - narrow list_ws_specific_versions to ancestors: walking down as well made a root fan out over its entire live fork subtree, and each member costs an identity lookup plus an RLS switch and probe. Ancestors are bounded by the fork depth limit. - probe the leftovers table unqualified so rollback restores on a PG_SCHEMA install, where search_path is not public - drop the nativets client method for the removed edit_deploy_to endpoint Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: let a prod see its dev workspace in ws_specific, and stop the walk oscillating Descending into plain forks made a root fan out over its whole live fork subtree, but a dev workspace is the paired editable environment rather than a throwaway copy, so a prod should still see it. There is at most one per parent and attach rejects nested dev chains, so that edge stays bounded. The edges run both ways, so the recursion never converged: it bounced parent<->dev until the depth cap on every call, 33 rows for a two-member set. A visited-path guard ends the walk when nothing new is reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep dev pairings unnested, gate the delete broadcast, cover the ws_specific walk Fifth review round: - a root that already owns a dev workspace no longer converts: linking it under its deploy target would leave that dev nested beneath a fork, the shape attach_dev_workspace refuses to create. The link is preserved instead. - broadcast a lineage change on delete only when descendants are orphaned. Deleting a leaf, which ephemeral fork churn does constantly, changes nobody else's resolution and was making every replica drop its whole tag cache. - call list_ws_specific_versions in a test. plpgsql defers everything past a raw parse to the first call, so replaying the migration only proved it parses. - use unwrap_or_default for the descendant sweeps, which run after the transaction has committed; a transient failure must not fail the request - trim the traversal comment to the four-line limit Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: cache the renamed tally query and clear instance alerts on conversion The integration test's query was never cached: `cargo sqlx prepare` without --all-targets skips test targets entirely, and renaming its fixture workspace changed the query text. Regenerated with --all-targets --features all_sqlx_features,private, which is what lets the EE-gated otel test compile. Also from review: - clear error_handler_fallback_to_instance_alerts on converted workspaces. Dispatch ignores it once a parent exists, but the settings page keeps submitting the stored true, which the API rejects on a fork. - restore the schema summary row to the file's name: columns format and put it back in alphabetical order Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: never cache an unresolvable tag workspace, and unadvertise the removed endpoint - lookup_tag_workspace cached a "no row" result as self-resolution. A rename resolves the new id before its row lands, so a fork could be pinned to its own wm-fork-* id -- which nothing serves -- for the whole TTL, and its schedules kept re-pushing onto that dead tag. Fall back for the call without caching, matching how the error path already behaved. - change_workspace_id swept its children but never itself. Sweep the new and old ids and broadcast unconditionally, since a rename always changes lineage. - openapi-deref.{json,yaml} are served to clients via include_str!, so they were advertising edit_deploy_to after it started 404ing. The audit-action enum keeps the entry: historical rows still carry it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: align the served YAML spec with the JSON one and correct two comments - the YAML deref lost the removed path but kept deploy_to on get_settings, so the two served specs disagreed. Both are now identical. - the rename-sweep comment blamed cached-unresolvable lookups, which the same commit stopped caching. The real reason is that workspace ids are reclaimable, so a new id can carry a previous occupant's resolution. - the instance-alert comment claimed the settings page submits the stored true and gets a 400. It hides the option on a fork and sends false; the hazard is the value outliving the pairing and re-enabling alerts after a detach. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 82da6cb2bafeda18acd6b70c599013a12117ecb0 This commit updates the EE repository reference after PR #694 was merged in windmill-ee-private. Previous ee-repo-ref: f9ddf6a75aa13d1c13a3d7216a361a96f75ca435 New ee-repo-ref: 82da6cb2bafeda18acd6b70c599013a12117ecb0 Automated by sync-ee-ref workflow. * fix: grant the deploy_to preservation table to the windmill roles * test: drop the one-shot migration tests, keep the ws_specific execution guard The two conversion tests replayed the migration against the fully-migrated schema, which is not how it runs -- in production it runs mid-sequence against the schema as of that point. A later migration touching workspace or workspace_settings would break them without breaking anything real, and sqlx checksums already freeze a released migration. They earned their keep finding the archived-claimant and nested-dev cases during development; there is nothing left for them to guard. list_ws_specific_versions is different: it is live, no caller exercises it, and plpgsql only parses a function body until first call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: invalidate a reclaimed fork id cluster-wide without flushing every entry Gating the delete broadcast on orphaned descendants stopped leaf churn flushing every replica, but fork ids are reclaimable: the deleting process invalidated locally while every other replica kept the old parent for the TTL, so a job pushed in a recreated fork routed to the previous parent's tag. The broadcast payload now carries meaning. A workspace id drops that one entry, used for leaf deletion where exactly one id changed what it denotes. The `*` sentinel drops everything, used for attach, detach, archive, rename and deletions that orphan descendants -- reshaping a subtree no single id names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: name the right broadcast for each invalidation case * docs: attach does invalidate the tag cache; the resolver walks the whole chain --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
346 lines
16 KiB
PL/PgSQL
346 lines
16 KiB
PL/PgSQL
-- `workspace_settings.deploy_to` (2023) and `workspace.parent_workspace_id` (2025) both expressed
|
|
-- "which workspace does this one deploy into". Fork creation and dev-workspace attach seeded both,
|
|
-- but nothing kept them in agreement, so every reader had to pick one and they disagreed. Fold the
|
|
-- surviving `deploy_to` pairs into the lineage and drop the column.
|
|
--
|
|
-- A converted pair becomes a dev workspace when it can: that is what a long-lived staging paired
|
|
-- with prod actually is, and it keeps its own tag domain and promotion mode, so no worker
|
|
-- configuration changes underneath it. Only one dev workspace is allowed per parent
|
|
-- (`workspace_canonical_dev_idx`) and the app only ever attaches a dev to a root, so a pair becomes
|
|
-- a plain fork when it fails either test. Those keep their link but borrow the parent's job tags;
|
|
-- an admin can re-attach one of them as the dev workspace afterwards.
|
|
--
|
|
-- Chains (`dev -> staging -> prod`) convert too: `parent_workspace_id` represents them natively, up
|
|
-- to the depth-20 backstop that billing and count resolution walk.
|
|
|
|
-- Whatever the lineage genuinely cannot express is preserved here rather than destroyed with the
|
|
-- column. `deploy_to` was admin-settable to any workspace, so these rows are real configuration; an
|
|
-- operator needs to see what was dropped, and the down migration restores from this table.
|
|
CREATE TABLE workspace_deploy_to_unmigrated (
|
|
workspace_id VARCHAR(50) PRIMARY KEY REFERENCES workspace(id) ON DELETE CASCADE,
|
|
deploy_to VARCHAR(255) NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
COMMENT ON TABLE workspace_deploy_to_unmigrated IS
|
|
'Legacy workspace_settings.deploy_to links that could not be expressed as fork lineage when the column was dropped. Written once by migration 20260730080304; never written by the application. Dropped again by that migration when every link converted, so it only exists where something was preserved.';
|
|
|
|
-- Tables created after the one-time GRANT ALL in 20250205131523 need explicit grants: ALTER
|
|
-- DEFAULT PRIVILEGES only covers objects created by the role that set them. Without these the
|
|
-- operator this table exists for cannot read it through the application's role.
|
|
GRANT ALL ON workspace_deploy_to_unmigrated TO windmill_user;
|
|
GRANT ALL ON workspace_deploy_to_unmigrated TO windmill_admin;
|
|
|
|
DO $$
|
|
DECLARE
|
|
leftover RECORD;
|
|
demoted RECORD;
|
|
converted_count INT;
|
|
dev_count INT;
|
|
BEGIN
|
|
-- Rows that could become lineage at all: a live source with no parent yet, pointing at a live
|
|
-- workspace other than itself.
|
|
CREATE TEMP TABLE eligible ON COMMIT DROP AS
|
|
SELECT ws.workspace_id, ws.deploy_to
|
|
FROM workspace_settings ws
|
|
JOIN workspace src ON src.id = ws.workspace_id
|
|
JOIN workspace tgt ON tgt.id = ws.deploy_to
|
|
WHERE ws.deploy_to IS NOT NULL
|
|
AND ws.deploy_to <> ws.workspace_id
|
|
AND NOT src.deleted AND NOT tgt.deleted
|
|
AND src.parent_workspace_id IS NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM workspace d
|
|
WHERE d.parent_workspace_id = ws.workspace_id
|
|
AND d.is_dev_workspace AND NOT d.deleted
|
|
);
|
|
|
|
-- Cycle detection has to run over the lineage as it would exist AFTER conversion, not over the
|
|
-- `deploy_to` graph alone: a root whose target is one of its own existing forks would close a
|
|
-- loop that no `deploy_to` edge reveals. Every eligible source is parentless, so the combined
|
|
-- edge set still gives each node at most one parent.
|
|
CREATE TEMP TABLE edge ON COMMIT DROP AS
|
|
SELECT id AS child, parent_workspace_id AS parent
|
|
FROM workspace WHERE parent_workspace_id IS NOT NULL
|
|
UNION ALL
|
|
SELECT workspace_id, deploy_to FROM eligible;
|
|
|
|
CREATE TEMP TABLE chain_info ON COMMIT DROP AS
|
|
WITH RECURSIVE walk(start_id, cur_id, depth, path, cyclic) AS (
|
|
SELECT e.child, e.parent, 1, ARRAY[e.child, e.parent], e.parent = e.child
|
|
FROM edge e
|
|
UNION ALL
|
|
SELECT w.start_id, nxt.parent, w.depth + 1,
|
|
w.path || nxt.parent, nxt.parent = ANY(w.path)
|
|
FROM walk w
|
|
JOIN edge nxt ON nxt.child = w.cur_id
|
|
WHERE NOT w.cyclic AND w.depth < 25
|
|
)
|
|
SELECT start_id, bool_or(cyclic) AS cyclic, max(depth) AS chain_depth
|
|
FROM walk GROUP BY start_id;
|
|
|
|
-- Every remaining link gets a verdict. A fork whose `deploy_to` already names its parent is the
|
|
-- redundant seed every fork carried, so dropping it loses nothing and it is not reported.
|
|
CREATE TEMP TABLE classified ON COMMIT DROP AS
|
|
SELECT ws.workspace_id, ws.deploy_to,
|
|
CASE
|
|
WHEN src.parent_workspace_id = ws.deploy_to THEN 'redundant'
|
|
WHEN src.deleted THEN 'source workspace is archived'
|
|
WHEN ws.deploy_to = ws.workspace_id THEN 'self-reference'
|
|
WHEN tgt.id IS NULL THEN 'target workspace does not exist'
|
|
WHEN tgt.deleted THEN 'target workspace is archived'
|
|
WHEN src.parent_workspace_id IS NOT NULL
|
|
THEN 'already a fork, pointing somewhere other than its parent'
|
|
WHEN EXISTS (
|
|
SELECT 1 FROM workspace d
|
|
WHERE d.parent_workspace_id = ws.workspace_id
|
|
AND d.is_dev_workspace AND NOT d.deleted
|
|
) THEN 'source owns a dev workspace; linking it would nest that dev under a fork'
|
|
WHEN ci.cyclic THEN 'linking it would form a cycle in the workspace lineage'
|
|
WHEN ci.chain_depth > 20 THEN 'chain exceeds the lineage depth limit'
|
|
END AS reason
|
|
FROM workspace_settings ws
|
|
JOIN workspace src ON src.id = ws.workspace_id
|
|
LEFT JOIN workspace tgt ON tgt.id = ws.deploy_to
|
|
LEFT JOIN chain_info ci ON ci.start_id = ws.workspace_id
|
|
WHERE ws.deploy_to IS NOT NULL;
|
|
|
|
INSERT INTO workspace_deploy_to_unmigrated (workspace_id, deploy_to, reason)
|
|
SELECT workspace_id, deploy_to, reason
|
|
FROM classified WHERE reason IS NOT NULL AND reason <> 'redundant';
|
|
|
|
FOR leftover IN SELECT workspace_id, deploy_to, reason FROM workspace_deploy_to_unmigrated
|
|
LOOP
|
|
RAISE WARNING 'deploy_to unification: % -> % kept in workspace_deploy_to_unmigrated (%)',
|
|
leftover.workspace_id, leftover.deploy_to, leftover.reason;
|
|
END LOOP;
|
|
|
|
CREATE TEMP TABLE convertible ON COMMIT DROP AS
|
|
SELECT c.workspace_id, c.deploy_to,
|
|
-- Sole claimant on a free root that has no dev workspace yet, mirroring what
|
|
-- `attach_dev_workspace` would allow. Anything else stays a plain fork.
|
|
(COUNT(*) OVER (PARTITION BY c.deploy_to) = 1
|
|
AND tgt.parent_workspace_id IS NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM workspace d
|
|
WHERE d.parent_workspace_id = c.deploy_to
|
|
AND d.is_dev_workspace AND NOT d.deleted
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM classified c2
|
|
WHERE c2.workspace_id = c.deploy_to AND c2.reason IS NULL
|
|
)) AS as_dev
|
|
FROM classified c
|
|
JOIN workspace tgt ON tgt.id = c.deploy_to
|
|
WHERE c.reason IS NULL;
|
|
|
|
FOR demoted IN SELECT workspace_id, deploy_to FROM convertible WHERE NOT as_dev
|
|
LOOP
|
|
RAISE NOTICE 'deploy_to unification: % -> % becomes a plain fork (target is not a free root); its jobs will use the parent''s tags',
|
|
demoted.workspace_id, demoted.deploy_to;
|
|
END LOOP;
|
|
|
|
UPDATE workspace w
|
|
SET parent_workspace_id = c.deploy_to,
|
|
is_dev_workspace = c.as_dev
|
|
FROM convertible c
|
|
WHERE w.id = c.workspace_id;
|
|
GET DIAGNOSTICS converted_count = ROW_COUNT;
|
|
|
|
-- Dispatch ignores this flag once a workspace has a parent, but the stored `true` outlives the
|
|
-- pairing: detaching later would silently re-enable instance alerting nobody asked for.
|
|
-- `attach_dev_workspace` clears it for the same reason.
|
|
UPDATE workspace_settings ws
|
|
SET error_handler_fallback_to_instance_alerts = false
|
|
FROM convertible c
|
|
WHERE c.workspace_id = ws.workspace_id
|
|
AND ws.error_handler_fallback_to_instance_alerts;
|
|
|
|
-- A converted workspace is now parent-managed, exactly as if `attach_dev_workspace` had run.
|
|
-- That path also strips git-sync state that would otherwise keep pulling and pushing against
|
|
-- the workspace's pre-conversion tracked branch: promotion repos are dropped, and auto-pull and
|
|
-- fork PRs are cleared on the rest. Any managed webhook is left registered but inert -- the
|
|
-- migration cannot call GitHub -- and is cleaned up on the next settings save.
|
|
UPDATE workspace_settings ws
|
|
SET git_sync = jsonb_set(
|
|
ws.git_sync,
|
|
'{repositories}',
|
|
COALESCE((
|
|
SELECT jsonb_agg(
|
|
(elem - 'auto_pull' - 'open_pr_error')
|
|
|| jsonb_build_object('fork_open_prs', false))
|
|
FROM jsonb_array_elements(ws.git_sync->'repositories') AS elem
|
|
WHERE COALESCE((elem->>'use_individual_branch')::boolean, false) = false
|
|
), '[]'::jsonb)
|
|
)
|
|
FROM convertible c
|
|
WHERE c.workspace_id = ws.workspace_id
|
|
AND jsonb_typeof(ws.git_sync->'repositories') = 'array';
|
|
|
|
SELECT count(*) INTO dev_count FROM convertible WHERE as_dev;
|
|
RAISE NOTICE 'deploy_to unification: linked % workspace(s) to their parent (% as dev workspaces), % preserved in workspace_deploy_to_unmigrated',
|
|
converted_count, dev_count, (SELECT count(*) FROM workspace_deploy_to_unmigrated);
|
|
|
|
-- Nothing to preserve is the normal outcome; leaving an empty table behind on every instance
|
|
-- forever buys nothing. It survives only where it holds something an operator needs to see.
|
|
IF NOT EXISTS (SELECT 1 FROM workspace_deploy_to_unmigrated) THEN
|
|
DROP TABLE workspace_deploy_to_unmigrated;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Resolve workspace-specific resources/variables over the fork lineage instead of the `deploy_to`
|
|
-- graph. Only the traversal changes; the per-workspace RLS fan-out below is unchanged.
|
|
--
|
|
-- Walks up through ancestors and down only to a dev workspace: descending into plain forks would
|
|
-- fan a root out over its whole live fork subtree, and each member costs an RLS switch and probe.
|
|
CREATE OR REPLACE FUNCTION list_ws_specific_versions(
|
|
seed_workspace TEXT,
|
|
user_email TEXT,
|
|
item_kind TEXT,
|
|
item_path TEXT
|
|
) RETURNS TABLE(ws VARCHAR) AS $$
|
|
DECLARE
|
|
rel RECORD;
|
|
usr_row RECORD;
|
|
user_perms TEXT[];
|
|
groups_csv TEXT;
|
|
pgroups_csv TEXT;
|
|
folders_read_csv TEXT;
|
|
folders_write_csv TEXT;
|
|
item_exists BOOLEAN;
|
|
is_super BOOLEAN;
|
|
BEGIN
|
|
IF item_kind NOT IN ('resource', 'variable') THEN
|
|
RAISE EXCEPTION 'Invalid kind: %', item_kind;
|
|
END IF;
|
|
|
|
SELECT COALESCE(super_admin, false) INTO is_super
|
|
FROM password WHERE email = user_email;
|
|
is_super := COALESCE(is_super, false);
|
|
|
|
BEGIN
|
|
FOR rel IN
|
|
WITH RECURSIVE related_workspaces(ws_id, depth, seen) AS (
|
|
SELECT seed_workspace::VARCHAR, 0, ARRAY[seed_workspace::VARCHAR]
|
|
UNION ALL
|
|
SELECT step.next_id, r.depth + 1, r.seen || step.next_id
|
|
FROM related_workspaces r
|
|
CROSS JOIN LATERAL (
|
|
SELECT CASE WHEN w.id = r.ws_id THEN w.parent_workspace_id ELSE w.id END
|
|
AS next_id
|
|
FROM workspace w
|
|
WHERE (w.id = r.ws_id AND w.parent_workspace_id IS NOT NULL)
|
|
OR (w.parent_workspace_id = r.ws_id
|
|
AND w.is_dev_workspace AND NOT w.deleted)
|
|
) step
|
|
-- The edges run both ways, so without this the walk bounces parent<->dev until it
|
|
-- hits the depth cap on every call regardless of how few workspaces are related.
|
|
WHERE r.depth < 32 AND NOT (step.next_id = ANY(r.seen))
|
|
)
|
|
SELECT DISTINCT r.ws_id
|
|
FROM related_workspaces r
|
|
INNER JOIN workspace w ON w.id = r.ws_id AND w.deleted = false
|
|
LOOP
|
|
SELECT u.username, u.is_admin
|
|
INTO usr_row
|
|
FROM usr u
|
|
WHERE u.email = user_email
|
|
AND u.workspace_id = rel.ws_id
|
|
AND u.disabled = false;
|
|
|
|
IF NOT FOUND AND NOT is_super THEN
|
|
CONTINUE;
|
|
END IF;
|
|
|
|
IF NOT FOUND THEN
|
|
-- super admin without a usr row in this workspace: synthesize an
|
|
-- admin identity so RLS is bypassed (windmill_admin role).
|
|
usr_row.username := user_email;
|
|
usr_row.is_admin := true;
|
|
groups_csv := '';
|
|
pgroups_csv := '';
|
|
folders_read_csv := '';
|
|
folders_write_csv := '';
|
|
ELSE
|
|
SELECT
|
|
COALESCE(string_agg(g, ','), ''),
|
|
COALESCE(string_agg('g/' || g, ','), '')
|
|
INTO groups_csv, pgroups_csv
|
|
FROM (
|
|
SELECT group_ AS g FROM usr_to_group
|
|
WHERE usr_to_group.usr = usr_row.username
|
|
AND usr_to_group.workspace_id = rel.ws_id
|
|
UNION ALL
|
|
SELECT igroup FROM email_to_igroup WHERE email = user_email
|
|
) gs;
|
|
|
|
user_perms := ARRAY['u/' || usr_row.username] || ARRAY(
|
|
SELECT 'g/' || g FROM (
|
|
SELECT group_ AS g FROM usr_to_group
|
|
WHERE usr = usr_row.username AND workspace_id = rel.ws_id
|
|
UNION ALL
|
|
SELECT igroup FROM email_to_igroup WHERE email = user_email
|
|
) gs2
|
|
);
|
|
|
|
-- folders_read: every folder the user can see (write implies read);
|
|
-- folders_write: only those granting write access.
|
|
WITH user_folders AS (
|
|
SELECT name, EXISTS (
|
|
SELECT 1 FROM jsonb_each_text(extra_perms) t
|
|
WHERE t.key = ANY(user_perms) AND t.value::boolean IS true
|
|
) AS is_write
|
|
FROM folder
|
|
WHERE extra_perms ?| user_perms AND folder.workspace_id = rel.ws_id
|
|
)
|
|
SELECT
|
|
COALESCE(string_agg(name, ','), ''),
|
|
COALESCE(string_agg(name, ',') FILTER (WHERE is_write), '')
|
|
INTO folders_read_csv, folders_write_csv
|
|
FROM user_folders;
|
|
|
|
IF is_super THEN
|
|
usr_row.is_admin := true;
|
|
END IF;
|
|
END IF;
|
|
|
|
PERFORM set_session_context(
|
|
usr_row.is_admin,
|
|
usr_row.username,
|
|
groups_csv,
|
|
pgroups_csv,
|
|
folders_read_csv,
|
|
folders_write_csv
|
|
);
|
|
|
|
EXECUTE format(
|
|
'SELECT EXISTS(SELECT 1 FROM %I WHERE workspace_id = $1 AND path = $2)',
|
|
item_kind
|
|
)
|
|
INTO item_exists
|
|
USING rel.ws_id, item_path;
|
|
|
|
IF item_exists THEN
|
|
ws := rel.ws_id;
|
|
RETURN NEXT;
|
|
END IF;
|
|
END LOOP;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
-- Reset to a deny-default state before re-raising so a half-set
|
|
-- session context can't leak past the failed call.
|
|
PERFORM set_session_context(false, '', '', '', '', '');
|
|
RAISE;
|
|
END;
|
|
|
|
-- Reset to a deny-default state on the happy path too. SET LOCAL is
|
|
-- transaction-scoped so this also unwinds at transaction end, but
|
|
-- being explicit defends against the function being called inside a
|
|
-- longer outer transaction.
|
|
PERFORM set_session_context(false, '', '', '', '', '');
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Dropping the column takes `workspace_settings_deploy_to_idx` with it; the new traversal is served
|
|
-- by `workspace_parent_idx`.
|
|
ALTER TABLE workspace_settings DROP COLUMN deploy_to;
|