From 7af24f59c3ecb100557960a8551fd0827cff32d2 Mon Sep 17 00:00:00 2001 From: Jonathan Griffin Date: Mon, 28 Apr 2025 11:14:32 +0200 Subject: [PATCH] add many-to-many relationship between groups and users --- ee/api/routers/scim/groups.py | 101 +++++++++--------- ee/api/routers/scim/users.py | 84 +++++++++++++-- .../db/init_dbs/postgresql/init_schema.sql | 11 +- 3 files changed, 133 insertions(+), 63 deletions(-) diff --git a/ee/api/routers/scim/groups.py b/ee/api/routers/scim/groups.py index 433dbc75b..774e0815b 100644 --- a/ee/api/routers/scim/groups.py +++ b/ee/api/routers/scim/groups.py @@ -78,13 +78,16 @@ def get_provider_resource_chunk( """ SELECT groups.*, - users_data.array as users + COALESCE( + ( + SELECT json_agg(users) + FROM public.user_group + JOIN public.users USING (user_id) + WHERE user_group.group_id = groups.group_id + ), + '[]' + ) AS users FROM public.groups - LEFT JOIN LATERAL ( - SELECT json_agg(users) AS array - FROM public.users - WHERE users.group_id = groups.group_id - ) users_data ON true WHERE groups.tenant_id = %(tenant_id)s LIMIT %(limit)s OFFSET %(offset)s; @@ -108,13 +111,16 @@ def get_provider_resource( """ SELECT groups.*, - users_data.array as users + COALESCE( + ( + SELECT json_agg(users) + FROM public.user_group + JOIN public.users USING (user_id) + WHERE user_group.group_id = groups.group_id + ), + '[]' + ) AS users FROM public.groups - LEFT JOIN LATERAL ( - SELECT json_agg(users) AS array - FROM public.users - WHERE users.group_id = groups.group_id - ) users_data ON true WHERE groups.tenant_id = %(tenant_id)s AND groups.group_id = %(group_id)s @@ -188,26 +194,24 @@ def create_provider_resource( VALUES ({value_clause}) RETURNING * ), - linked_users AS ( - UPDATE public.users - SET - group_id = g.group_id, - updated_at = now() + ugs AS ( + INSERT INTO public.user_group (user_id, group_id) + SELECT users.user_id, g.group_id FROM g - WHERE - users.user_id = ANY({user_id_clause}) - AND users.deleted_at IS NULL - AND users.tenant_id = {tenant_id} + JOIN public.users ON users.user_id = ANY({user_id_clause}) RETURNING * ) SELECT g.*, - COALESCE(users_data.array, '[]') as users + COALESCE( + ( + SELECT json_agg(users) + FROM ugs + JOIN public.users USING (user_id) + ), + '[]' + ) AS users FROM g - LEFT JOIN LATERAL ( - SELECT json_agg(lu) AS array - FROM linked_users AS lu - ) users_data ON true LIMIT 1; """ ) @@ -245,6 +249,12 @@ def _update_resource_sql( cur.mogrify("%s", (user_id,)).decode("utf-8") for user_id in user_ids ] user_id_clause = f"ARRAY[{', '.join(user_id_fragments)}]::int[]" + cur.execute( + f""" + DELETE FROM public.user_group + WHERE user_group.group_id = {group_id} + """ + ) cur.execute( f""" WITH @@ -256,36 +266,25 @@ def _update_resource_sql( AND groups.tenant_id = {tenant_id} RETURNING * ), - unlinked_users AS ( - UPDATE public.users - SET - group_id = null, - updated_at = now() - WHERE - users.group_id = {group_id} - AND users.user_id <> ALL({user_id_clause}) - AND users.deleted_at IS NULL - AND users.tenant_id = {tenant_id} - ), - linked_users AS ( - UPDATE public.users - SET - group_id = {group_id}, - updated_at = now() - WHERE - users.user_id = ANY({user_id_clause}) - AND users.deleted_at IS NULL - AND users.tenant_id = {tenant_id} + linked_user_group AS ( + INSERT INTO public.user_group (user_id, group_id) + SELECT users.user_id, g.group_id + FROM g + JOIN public.users ON users.user_id = ANY({user_id_clause}) + WHERE users.deleted_at IS NULL AND users.tenant_id = {tenant_id} RETURNING * ) SELECT g.*, - COALESCE(users_data.array, '[]') as users + COALESCE( + ( + SELECT json_agg(users) + FROM linked_user_group + JOIN public.users USING (user_id) + ), + '[]' + ) AS users FROM g - LEFT JOIN LATERAL ( - SELECT json_agg(lu) AS array - FROM linked_users AS lu - ) users_data ON true LIMIT 1; """ ) diff --git a/ee/api/routers/scim/users.py b/ee/api/routers/scim/users.py index 98e9b3ded..2444f3f07 100644 --- a/ee/api/routers/scim/users.py +++ b/ee/api/routers/scim/users.py @@ -156,6 +156,13 @@ def convert_provider_resource_to_client_resource( "displayName": provider_resource["name"] or provider_resource["email"], "userType": provider_resource.get("role_name"), "active": provider_resource["deleted_at"] is None, + "groups": [ + { + "value": str(group["group_id"]), + "$ref": f"Groups/{group['group_id']}", + } + for group in provider_resource["groups"] + ], } @@ -185,7 +192,16 @@ def get_provider_resource_chunk( """ SELECT users.*, - roles.name AS role_name + roles.name AS role_name, + COALESCE( + ( + SELECT json_agg(groups) + FROM public.user_group + JOIN public.groups USING (group_id) + WHERE user_group.user_id = users.user_id + ), + '[]' + ) AS groups FROM public.users LEFT JOIN public.roles USING (role_id) WHERE @@ -209,7 +225,16 @@ def get_provider_resource( """ SELECT users.*, - roles.name AS role_name + roles.name AS role_name, + COALESCE( + ( + SELECT json_agg(groups) + FROM public.user_group + JOIN public.groups USING (group_id) + WHERE user_group.user_id = users.user_id + ), + '[]' + ) AS groups FROM public.users LEFT JOIN public.roles USING (role_id) WHERE @@ -257,8 +282,18 @@ def create_provider_resource( ) SELECT u.*, - roles.name as role_name - FROM u LEFT JOIN public.roles USING (role_id); + roles.name as role_name, + COALESCE( + ( + SELECT json_agg(groups) + FROM public.user_group + JOIN public.groups USING (group_id) + WHERE user_group.user_id = u.user_id + ), + '[]' + ) AS groups + FROM u + LEFT JOIN public.roles USING (role_id) """, { "tenant_id": tenant_id, @@ -303,7 +338,16 @@ def restore_provider_resource( ) SELECT u.*, - roles.name as role_name + roles.name as role_name, + COALESCE( + ( + SELECT json_agg(groups) + FROM public.user_group + JOIN public.groups USING (group_id) + WHERE user_group.user_id = u.user_id + ), + '[]' + ) AS groups FROM u LEFT JOIN public.roles USING (role_id); """, { @@ -346,7 +390,16 @@ def rewrite_provider_resource( ) SELECT u.*, - roles.name as role_name + roles.name as role_name, + COALESCE( + ( + SELECT json_agg(groups) + FROM public.user_group + JOIN public.groups USING (group_id) + WHERE user_group.user_id = u.user_id + ), + '[]' + ) AS groups FROM u LEFT JOIN public.roles USING (role_id); """, { @@ -377,7 +430,8 @@ def update_provider_resource( ).decode("utf-8") set_fragments.append(fragment) set_clause = ", ".join(set_fragments) - query = f""" + cur.execute( + f""" WITH u AS ( UPDATE public.users SET {set_clause} @@ -389,7 +443,17 @@ def update_provider_resource( ) SELECT u.*, - roles.name as role_name - FROM u LEFT JOIN public.roles USING (role_id);""" - cur.execute(query) + roles.name as role_name, + COALESCE( + ( + SELECT json_agg(groups) + FROM public.user_group + JOIN public.groups USING (group_id) + WHERE user_group.user_id = u.user_id + ), + '[]' + ) AS groups + FROM u LEFT JOIN public.roles USING (role_id) + """ + ) return cur.fetchone() diff --git a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql index 521079621..a62a34cbb 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -162,12 +162,19 @@ CREATE TABLE public.users origin text NULL DEFAULT NULL, role_id integer REFERENCES public.roles (role_id) ON DELETE SET NULL, internal_id text NULL DEFAULT NULL, - service_account bool NOT NULL DEFAULT FALSE, - group_id integer REFERENCES public.groups (group_id) ON DELETE SET NULL + service_account bool NOT NULL DEFAULT FALSE ); CREATE INDEX users_tenant_id_deleted_at_N_idx ON public.users (tenant_id) WHERE deleted_at ISNULL; CREATE INDEX users_name_gin_idx ON public.users USING GIN (name gin_trgm_ops); +CREATE TABLE public.user_group +( + user_group_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id integer REFERENCES public.users (user_id) ON DELETE CASCADE, + group_id integer REFERENCES public.groups (group_id) ON DELETE CASCADE, + UNIQUE (user_id, group_id) +); + CREATE TABLE public.basic_authentication ( user_id integer NOT NULL REFERENCES public.users (user_id) ON DELETE CASCADE,