1+ -- Rebase permission groups from organization-scoped to workspace-scoped.
2+ -- Each existing org-scoped permission_group is cloned onto every workspace
3+ -- owned by that org; members are copied to each clone only if the user has
4+ -- workspace-level permissions on the target workspace.
5+
6+ -- 0. Backfill workspace -> organization links for grandfathered workspaces whose
7+ -- billed account user is the sole owner of exactly one organization. This is a
8+ -- best-effort reconciliation: migration 0192 defaulted every pre-existing
9+ -- workspace to `grandfathered_shared` with `organization_id = NULL`, but many
10+ -- of those workspaces belong to users who own a single org. Without this link,
11+ -- the permission-group clone step below would drop all access control data for
12+ -- those workspaces. We only attach when ownership is unambiguous (user owns
13+ -- exactly one org) to avoid silently binding a workspace to the wrong org.
14+ UPDATE " workspace" w
15+ SET " organization_id" = owner_orgs." organization_id" ,
16+ " workspace_mode" = ' organization' ::" workspace_mode"
17+ FROM (
18+ SELECT m." user_id" , MIN (m." organization_id" ) AS " organization_id"
19+ FROM " member" m
20+ WHERE m." role" = ' owner'
21+ GROUP BY m." user_id"
22+ HAVING COUNT (* ) = 1
23+ ) AS owner_orgs
24+ WHERE w." organization_id" IS NULL
25+ AND w." workspace_mode" = ' grandfathered_shared'
26+ AND w." billed_account_user_id" = owner_orgs." user_id" ;-- > statement-breakpoint
27+
28+ -- 1. Add workspace_id as nullable so existing rows can coexist during the data migration.
29+ ALTER TABLE " permission_group" ADD COLUMN " workspace_id" text ;-- > statement-breakpoint
30+ ALTER TABLE " permission_group" ADD CONSTRAINT " permission_group_workspace_id_workspace_id_fk" FOREIGN KEY (" workspace_id" ) REFERENCES " public" ." workspace" (" id" ) ON DELETE cascade ON UPDATE no action;-- > statement-breakpoint
31+
32+ -- 2. Materialize a plan of (source permission group, target workspace, new clone id)
33+ -- so we can insert the clone rows AND the member rows with stable references.
34+ CREATE TEMP TABLE " __permission_group_clone_plan" (
35+ " source_id" text NOT NULL ,
36+ " cloned_id" text NOT NULL ,
37+ " workspace_id" text NOT NULL
38+ ) ON COMMIT DROP;-- > statement-breakpoint
39+
40+ INSERT INTO " __permission_group_clone_plan" (" source_id" , " cloned_id" , " workspace_id" )
41+ SELECT pg." id" , gen_random_uuid()::text , w." id"
42+ FROM " permission_group" pg
43+ JOIN " workspace" w ON w." organization_id" = pg." organization_id"
44+ WHERE pg." organization_id" IS NOT NULL ;-- > statement-breakpoint
45+
46+ -- 3. Create the workspace-scoped clone rows using the planned ids.
47+ INSERT INTO " permission_group" (
48+ " id" ,
49+ " workspace_id" ,
50+ " organization_id" ,
51+ " name" ,
52+ " description" ,
53+ " config" ,
54+ " created_by" ,
55+ " created_at" ,
56+ " updated_at" ,
57+ " auto_add_new_members"
58+ )
59+ SELECT
60+ plan." cloned_id" ,
61+ plan." workspace_id" ,
62+ NULL ,
63+ pg." name" ,
64+ pg." description" ,
65+ (pg." config" - ' hideEnvironmentTab' - ' hideTemplates' ),
66+ pg." created_by" ,
67+ now(),
68+ now(),
69+ pg." auto_add_new_members"
70+ FROM " __permission_group_clone_plan" plan
71+ JOIN " permission_group" pg ON pg." id" = plan." source_id" ;-- > statement-breakpoint
72+
73+ -- 4. Copy member rows to each workspace clone, but only if the user has
74+ -- workspace-level permissions on that target workspace.
75+ INSERT INTO " permission_group_member" (" id" , " permission_group_id" , " user_id" , " assigned_by" , " assigned_at" )
76+ SELECT
77+ gen_random_uuid()::text ,
78+ plan." cloned_id" ,
79+ m." user_id" ,
80+ m." assigned_by" ,
81+ m." assigned_at"
82+ FROM " __permission_group_clone_plan" plan
83+ JOIN " permission_group_member" m ON m." permission_group_id" = plan." source_id"
84+ WHERE EXISTS (
85+ SELECT 1 FROM " permissions" p
86+ WHERE p." entity_type" = ' workspace'
87+ AND p." entity_id" = plan." workspace_id"
88+ AND p." user_id" = m." user_id"
89+ ) OR EXISTS (
90+ SELECT 1 FROM " workspace" w
91+ WHERE w." id" = plan." workspace_id"
92+ AND w." owner_id" = m." user_id"
93+ );-- > statement-breakpoint
94+
95+ -- 5. Delete legacy org-scoped rows now that clones exist.
96+ DELETE FROM " permission_group_member"
97+ WHERE " permission_group_id" IN (
98+ SELECT " id" FROM " permission_group" WHERE " organization_id" IS NOT NULL
99+ );-- > statement-breakpoint
100+
101+ DELETE FROM " permission_group" WHERE " organization_id" IS NOT NULL ;-- > statement-breakpoint
102+
103+ -- 6. Enforce NOT NULL on workspace_id now that every surviving row has one.
104+ ALTER TABLE " permission_group" ALTER COLUMN " workspace_id" SET NOT NULL ;-- > statement-breakpoint
105+
106+ -- 7. Drop legacy structures and swap indexes.
107+ ALTER TABLE " permission_group" DROP CONSTRAINT " permission_group_organization_id_organization_id_fk" ;-- > statement-breakpoint
108+ DROP INDEX " permission_group_org_name_unique" ;-- > statement-breakpoint
109+ DROP INDEX " permission_group_org_auto_add_unique" ;-- > statement-breakpoint
110+ DROP INDEX " permission_group_member_user_id_unique" ;-- > statement-breakpoint
111+ ALTER TABLE " permission_group" DROP COLUMN " organization_id" ;-- > statement-breakpoint
112+ CREATE UNIQUE INDEX "permission_group_workspace_name_unique " ON " permission_group" USING btree (" workspace_id" ," name" );-- > statement-breakpoint
113+ CREATE UNIQUE INDEX "permission_group_workspace_auto_add_unique " ON " permission_group" USING btree (" workspace_id" ) WHERE auto_add_new_members = true;-- > statement-breakpoint
114+ CREATE UNIQUE INDEX "permission_group_member_group_user_unique " ON " permission_group_member" USING btree (" permission_group_id" ," user_id" );-- > statement-breakpoint
115+
116+ -- 8. Sweep any residual dead config keys from pre-existing workspace-scoped rows (if any).
117+ UPDATE " permission_group" SET " config" = (" config" - ' hideEnvironmentTab' - ' hideTemplates' ) WHERE " config" ? ' hideEnvironmentTab' OR " config" ? ' hideTemplates' ;
0 commit comments