diff --git a/ee/api/routers/scim/groups.py b/ee/api/routers/scim/groups.py index 774e0815b..53c6389ff 100644 --- a/ee/api/routers/scim/groups.py +++ b/ee/api/routers/scim/groups.py @@ -18,8 +18,6 @@ def convert_client_resource_update_input_to_provider_resource_update_input( result = {} if "displayName" in client_input: result["name"] = client_input["displayName"] - if "externalId" in client_input: - result["external_id"] = client_input["externalId"] if "members" in client_input: members = client_input["members"] or [] result["user_ids"] = [int(member["value"]) for member in members] @@ -32,15 +30,14 @@ def convert_provider_resource_to_client_resource( members = provider_resource["users"] or [] return { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], - "id": str(provider_resource["group_id"]), - "externalId": provider_resource["external_id"], + "id": str(provider_resource["role_id"]), "meta": { "resourceType": "Group", "created": provider_resource["created_at"].strftime("%Y-%m-%dT%H:%M:%SZ"), "lastModified": provider_resource["updated_at"].strftime( "%Y-%m-%dT%H:%M:%SZ" ), - "location": f"Groups/{provider_resource['group_id']}", + "location": f"Groups/{provider_resource['role_id']}", }, "displayName": provider_resource["name"], "members": [ @@ -60,8 +57,10 @@ def get_active_resource_count(tenant_id: int) -> int: cur.mogrify( """ SELECT COUNT(*) - FROM public.groups - WHERE groups.tenant_id = %(tenant_id)s + FROM public.roles + WHERE + roles.tenant_id = %(tenant_id)s + AND roles.deleted_at IS NULL """, {"tenant_id": tenant_id}, ) @@ -77,18 +76,19 @@ def get_provider_resource_chunk( cur.mogrify( """ SELECT - groups.*, + roles.*, COALESCE( ( SELECT json_agg(users) - FROM public.user_group - JOIN public.users USING (user_id) - WHERE user_group.group_id = groups.group_id + FROM public.users + WHERE users.role_id = roles.role_id ), '[]' ) AS users - FROM public.groups - WHERE groups.tenant_id = %(tenant_id)s + FROM public.roles + WHERE + roles.tenant_id = %(tenant_id)s + AND roles.deleted_at IS NULL LIMIT %(limit)s OFFSET %(offset)s; """, @@ -110,23 +110,23 @@ def get_provider_resource( cur.mogrify( """ SELECT - groups.*, + roles.*, COALESCE( ( SELECT json_agg(users) - FROM public.user_group - JOIN public.users USING (user_id) - WHERE user_group.group_id = groups.group_id + FROM public.users + WHERE users.role_id = roles.role_id ), '[]' ) AS users - FROM public.groups + FROM public.roles WHERE - groups.tenant_id = %(tenant_id)s - AND groups.group_id = %(group_id)s + roles.tenant_id = %(tenant_id)s + AND roles.role_id = %(resource_id)s + AND roles.deleted_at IS NULL LIMIT 1; """, - {"group_id": resource_id, "tenant_id": tenant_id}, + {"resource_id": resource_id, "tenant_id": tenant_id}, ) ) return cur.fetchone() @@ -135,7 +135,7 @@ def get_provider_resource( def get_provider_resource_from_unique_fields( **kwargs: dict[str, Any], ) -> ProviderResource | None: - # note(jon): we do not really use this for groups as we don't have unique values outside + # note(jon): we do not really use this for scim.groups (openreplay.roles) as we don't have unique values outside # of the primary key return None @@ -145,7 +145,6 @@ def convert_client_resource_creation_input_to_provider_resource_creation_input( ) -> ProviderInput: return { "name": client_input["displayName"], - "external_id": client_input.get("externalId"), "user_ids": [ int(member["value"]) for member in client_input.get("members", []) ], @@ -157,7 +156,6 @@ def convert_client_resource_rewrite_input_to_provider_resource_rewrite_input( ) -> ProviderInput: return { "name": client_input["displayName"], - "external_id": client_input.get("externalId"), "user_ids": [ int(member["value"]) for member in client_input.get("members", []) ], @@ -189,50 +187,37 @@ def create_provider_resource( cur.execute( f""" WITH - g AS ( - INSERT INTO public.groups ({column_clause}) + r AS ( + INSERT INTO public.roles ({column_clause}) VALUES ({value_clause}) RETURNING * ), - ugs 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}) + linked_users AS ( + UPDATE public.users + SET + updated_at = now(), + role_id = (SELECT r.role_id FROM r) + WHERE users.user_id = ANY({user_id_clause}) RETURNING * ) SELECT - g.*, + r.*, COALESCE( ( - SELECT json_agg(users) - FROM ugs - JOIN public.users USING (user_id) + SELECT json_agg(linked_users.*) + FROM linked_users ), '[]' ) AS users - FROM g + FROM r LIMIT 1; """ ) return cur.fetchone() -def delete_provider_resource(resource_id: ResourceId, tenant_id: int) -> None: - with pg_client.PostgresClient() as cur: - cur.execute( - cur.mogrify( - """ - DELETE FROM public.groups - WHERE groups.group_id = %(group_id)s AND groups.tenant_id = %(tenant_id)s; - """ - ), - {"tenant_id": tenant_id, "group_id": resource_id}, - ) - - def _update_resource_sql( - group_id: int, + resource_id: int, tenant_id: int, user_ids: list[int] | None = None, **kwargs: dict[str, Any], @@ -251,46 +236,71 @@ def _update_resource_sql( 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} + UPDATE public.users + SET role_id = NULL + WHERE users.role_id = {resource_id} """ ) cur.execute( f""" WITH - g AS ( - UPDATE public.groups + r AS ( + UPDATE public.roles SET {set_clause} WHERE - groups.group_id = {group_id} - AND groups.tenant_id = {tenant_id} + roles.role_id = {resource_id} + AND roles.tenant_id = {tenant_id} + AND roles.deleted_at IS NULL RETURNING * ), - 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} + linked_users AS ( + UPDATE public.users + SET + updated_at = now(), + role_id = {resource_id} + WHERE users.user_id = ANY({user_id_clause}) RETURNING * ) SELECT - g.*, + r.*, COALESCE( ( - SELECT json_agg(users) - FROM linked_user_group - JOIN public.users USING (user_id) + SELECT json_agg(linked_users.*) + FROM linked_users ), '[]' ) AS users - FROM g + FROM r LIMIT 1; """ ) return cur.fetchone() +def delete_provider_resource(resource_id: ResourceId, tenant_id: int) -> None: + _update_resource_sql( + resource_id=resource_id, + tenant_id=tenant_id, + deleted_at=datetime.now(), + ) + + +def restore_provider_resource( + resource_id: int, + tenant_id: int, + name: str, + **kwargs: dict[str, Any], +) -> dict[str, Any]: + return _update_resource_sql( + resource_id=resource_id, + tenant_id=tenant_id, + name=name, + created_at=datetime.now(), + deleted_at=None, + **kwargs, + ) + + def rewrite_provider_resource( resource_id: int, tenant_id: int, @@ -298,7 +308,7 @@ def rewrite_provider_resource( **kwargs: dict[str, Any], ) -> dict[str, Any]: return _update_resource_sql( - group_id=resource_id, + resource_id=resource_id, tenant_id=tenant_id, name=name, **kwargs, @@ -311,7 +321,7 @@ def update_provider_resource( **kwargs: dict[str, Any], ): return _update_resource_sql( - group_id=resource_id, + resource_id=resource_id, tenant_id=tenant_id, **kwargs, ) diff --git a/ee/api/routers/scim/users.py b/ee/api/routers/scim/users.py index 2444f3f07..ccd17e4df 100644 --- a/ee/api/routers/scim/users.py +++ b/ee/api/routers/scim/users.py @@ -17,9 +17,6 @@ def convert_client_resource_update_input_to_provider_resource_update_input( tenant_id: int, client_input: ClientInput ) -> ProviderInput: result = {} - if "userType" in client_input: - role = roles.get_role_by_name(tenant_id, client_input["userType"]) - result["role_id"] = role["roleId"] if role else None if "name" in client_input: # note(jon): we're currently not handling the case where the client # send patches of individual name components (e.g. name.middleName) @@ -38,10 +35,6 @@ def convert_client_resource_update_input_to_provider_resource_update_input( def convert_client_resource_rewrite_input_to_provider_resource_rewrite_input( tenant_id: int, client_input: ClientInput ) -> ProviderInput: - role_id = None - if "userType" in client_input: - role = roles.get_role_by_name(tenant_id, client_input["userType"]) - role_id = role["roleId"] if role else None name = client_input.get("name", {}).get("formatted") if not name: name = " ".join( @@ -61,7 +54,6 @@ def convert_client_resource_rewrite_input_to_provider_resource_rewrite_input( "email": client_input["userName"], "internal_id": client_input.get("externalId"), "name": name, - "role_id": role_id, } result = {k: v for k, v in result.items() if v is not None} return result @@ -70,10 +62,6 @@ def convert_client_resource_rewrite_input_to_provider_resource_rewrite_input( def convert_client_resource_creation_input_to_provider_resource_creation_input( tenant_id: int, client_input: ClientInput ) -> ProviderInput: - role_id = None - if "userType" in client_input: - role = roles.get_role_by_name(tenant_id, client_input["userType"]) - role_id = role["roleId"] if role else None name = client_input.get("name", {}).get("formatted") if not name: name = " ".join( @@ -93,7 +81,6 @@ def convert_client_resource_creation_input_to_provider_resource_creation_input( "email": client_input["userName"], "internal_id": client_input.get("externalId"), "name": name, - "role_id": role_id, } result = {k: v for k, v in result.items() if v is not None} return result @@ -124,7 +111,7 @@ def delete_provider_resource(resource_id: ResourceId, tenant_id: int) -> None: UPDATE public.users SET deleted_at = NULL, - updated_at = default + updated_at = now() WHERE users.user_id = %(user_id)s AND users.tenant_id = %(tenant_id)s @@ -137,6 +124,14 @@ def delete_provider_resource(resource_id: ResourceId, tenant_id: int) -> None: def convert_provider_resource_to_client_resource( provider_resource: ProviderResource, ) -> ClientResource: + groups = [] + if provider_resource["role_id"]: + groups.append( + { + "value": str(provider_resource["role_id"]), + "$ref": f"Groups/{provider_resource['role_id']}", + } + ) return { "id": str(provider_resource["user_id"]), "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], @@ -154,15 +149,8 @@ def convert_provider_resource_to_client_resource( "formatted": provider_resource["name"], }, "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"] - ], + "groups": groups, } @@ -190,20 +178,8 @@ def get_provider_resource_chunk( cur.execute( cur.mogrify( """ - SELECT - users.*, - 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 + SELECT * FROM public.users - LEFT JOIN public.roles USING (role_id) WHERE users.tenant_id = %(tenant_id)s AND users.deleted_at IS NULL @@ -223,20 +199,8 @@ def get_provider_resource( cur.execute( cur.mogrify( """ - SELECT - users.*, - 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 + SELECT * FROM public.users - LEFT JOIN public.roles USING (role_id) WHERE users.user_id = %(user_id)s AND users.tenant_id = %(tenant_id)s @@ -257,7 +221,6 @@ def create_provider_resource( tenant_id: int, name: str = "", internal_id: str | None = None, - role_id: int | None = None, ) -> ProviderResource: with pg_client.PostgresClient() as cur: cur.execute( @@ -268,39 +231,24 @@ def create_provider_resource( tenant_id, email, name, - internal_id, - role_id + internal_id ) VALUES ( %(tenant_id)s, %(email)s, %(name)s, - %(internal_id)s, - %(role_id)s + %(internal_id)s ) RETURNING * ) - SELECT - u.*, - 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 + SELECT * FROM u - LEFT JOIN public.roles USING (role_id) """, { "tenant_id": tenant_id, "email": email, "name": name, "internal_id": internal_id, - "role_id": role_id, }, ) ) @@ -312,7 +260,6 @@ def restore_provider_resource( email: str, name: str = "", internal_id: str | None = None, - role_id: int | None = None, **kwargs: dict[str, Any], ) -> ProviderResource: with pg_client.PostgresClient() as cur: @@ -326,7 +273,6 @@ def restore_provider_resource( email = %(email)s, name = %(name)s, internal_id = %(internal_id)s, - role_id = %(role_id)s, deleted_at = NULL, created_at = now(), updated_at = now(), @@ -336,26 +282,14 @@ def restore_provider_resource( WHERE users.email = %(email)s RETURNING * ) - SELECT - u.*, - 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); + SELECT * + FROM u """, { "tenant_id": tenant_id, "email": email, "name": name, "internal_id": internal_id, - "role_id": role_id, }, ) ) @@ -368,7 +302,6 @@ def rewrite_provider_resource( email: str, name: str = "", internal_id: str | None = None, - role_id: int | None = None, ): with pg_client.PostgresClient() as cur: cur.execute( @@ -380,7 +313,6 @@ def rewrite_provider_resource( email = %(email)s, name = %(name)s, internal_id = %(internal_id)s, - role_id = %(role_id)s, updated_at = now() WHERE users.user_id = %(user_id)s @@ -388,19 +320,8 @@ def rewrite_provider_resource( AND users.deleted_at IS NULL RETURNING * ) - SELECT - u.*, - 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); + SELECT * + FROM u """, { "tenant_id": tenant_id, @@ -408,7 +329,6 @@ def rewrite_provider_resource( "email": email, "name": name, "internal_id": internal_id, - "role_id": role_id, }, ) ) @@ -441,19 +361,8 @@ def update_provider_resource( AND users.deleted_at IS NULL RETURNING * ) - SELECT - u.*, - 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) + SELECT * + FROM u """ ) 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 a62a34cbb..dc231c2a0 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -118,20 +118,11 @@ CREATE TABLE public.roles protected bool NOT NULL DEFAULT FALSE, all_projects bool NOT NULL DEFAULT TRUE, created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), + updated_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), deleted_at timestamp NULL DEFAULT NULL, service_role bool NOT NULL DEFAULT FALSE ); -CREATE TABLE public.groups -( - group_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - tenant_id integer NOT NULL REFERENCES public.tenants (tenant_id) ON DELETE CASCADE, - external_id text, - name text NOT NULL, - created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), - updated_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc') -); - CREATE TYPE user_role AS ENUM ('owner','admin','member','service'); CREATE TABLE public.users @@ -167,14 +158,6 @@ CREATE TABLE public.users 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,