update GET Users api to be minimal working rfc version
This commit is contained in:
parent
13f46fe566
commit
bcfa421b8f
4 changed files with 363 additions and 220 deletions
|
|
@ -30,7 +30,7 @@ def create_new_member(tenant_id, email, invitation_token, admin, name, owner=Fal
|
||||||
query = cur.mogrify(f"""\
|
query = cur.mogrify(f"""\
|
||||||
WITH u AS (
|
WITH u AS (
|
||||||
INSERT INTO public.users (tenant_id, email, role, name, data, role_id)
|
INSERT INTO public.users (tenant_id, email, role, name, data, role_id)
|
||||||
VALUES (%(tenant_id)s, %(email)s, %(role)s, %(name)s, %(data)s,
|
VALUES (%(tenant_id)s, %(email)s, %(role)s, %(name)s, %(data)s,
|
||||||
(SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
(SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))))
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))))
|
||||||
|
|
@ -78,7 +78,7 @@ def restore_member(tenant_id, user_id, email, invitation_token, admin, name, own
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1)))
|
||||||
WHERE user_id=%(user_id)s
|
WHERE user_id=%(user_id)s
|
||||||
RETURNING
|
RETURNING
|
||||||
tenant_id,
|
tenant_id,
|
||||||
user_id,
|
user_id,
|
||||||
email,
|
email,
|
||||||
|
|
@ -104,7 +104,7 @@ def restore_member(tenant_id, user_id, email, invitation_token, admin, name, own
|
||||||
u.role_id,
|
u.role_id,
|
||||||
roles.name AS role_name,
|
roles.name AS role_name,
|
||||||
TRUE AS has_password
|
TRUE AS has_password
|
||||||
FROM au,u LEFT JOIN roles USING(tenant_id)
|
FROM au,u LEFT JOIN roles USING(tenant_id)
|
||||||
WHERE roles.role_id IS NULL OR roles.role_id = (SELECT u.role_id FROM u);""",
|
WHERE roles.role_id IS NULL OR roles.role_id = (SELECT u.role_id FROM u);""",
|
||||||
{"tenant_id": tenant_id, "user_id": user_id, "email": email,
|
{"tenant_id": tenant_id, "user_id": user_id, "email": email,
|
||||||
"role": "owner" if owner else "admin" if admin else "member", "name": name,
|
"role": "owner" if owner else "admin" if admin else "member", "name": name,
|
||||||
|
|
@ -240,7 +240,7 @@ def __get_invitation_link(invitation_token):
|
||||||
def allow_password_change(user_id, delta_min=10):
|
def allow_password_change(user_id, delta_min=10):
|
||||||
pass_token = secrets.token_urlsafe(8)
|
pass_token = secrets.token_urlsafe(8)
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""UPDATE public.basic_authentication
|
query = cur.mogrify(f"""UPDATE public.basic_authentication
|
||||||
SET change_pwd_expire_at = timezone('utc'::text, now()+INTERVAL '%(delta)s MINUTES'),
|
SET change_pwd_expire_at = timezone('utc'::text, now()+INTERVAL '%(delta)s MINUTES'),
|
||||||
change_pwd_token = %(pass_token)s
|
change_pwd_token = %(pass_token)s
|
||||||
WHERE user_id = %(user_id)s""",
|
WHERE user_id = %(user_id)s""",
|
||||||
|
|
@ -255,11 +255,11 @@ def get(user_id, tenant_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.tenant_id,
|
users.tenant_id,
|
||||||
email,
|
email,
|
||||||
role,
|
role,
|
||||||
users.name,
|
users.name,
|
||||||
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
|
|
@ -283,38 +283,25 @@ def get(user_id, tenant_id):
|
||||||
)
|
)
|
||||||
r = cur.fetchone()
|
r = cur.fetchone()
|
||||||
return helper.dict_to_camel_case(r)
|
return helper.dict_to_camel_case(r)
|
||||||
|
|
||||||
def get_by_uuid(user_uuid, tenant_id):
|
def get_by_uuid(user_uuid, tenant_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
"""
|
||||||
users.user_id,
|
SELECT *
|
||||||
users.tenant_id,
|
FROM public.users
|
||||||
email,
|
WHERE
|
||||||
role,
|
users.deleted_at IS NULL
|
||||||
users.name,
|
AND users.user_id = %(user_id)s
|
||||||
users.data,
|
AND users.tenant_id = %(tenant_id)s
|
||||||
users.internal_id,
|
LIMIT 1;
|
||||||
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
""",
|
||||||
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
{
|
||||||
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
"user_id": user_uuid,
|
||||||
origin,
|
"tenant_id": tenant_id,
|
||||||
role_id,
|
},
|
||||||
roles.name AS role_name,
|
)
|
||||||
roles.permissions,
|
|
||||||
roles.all_projects,
|
|
||||||
basic_authentication.password IS NOT NULL AS has_password,
|
|
||||||
users.service_account
|
|
||||||
FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
|
||||||
LEFT JOIN public.roles USING (role_id)
|
|
||||||
WHERE
|
|
||||||
users.data->>'user_id' = %(user_uuid)s
|
|
||||||
AND users.tenant_id = %(tenant_id)s
|
|
||||||
AND users.deleted_at IS NULL
|
|
||||||
AND (roles.role_id IS NULL OR roles.deleted_at IS NULL AND roles.tenant_id = %(tenant_id)s)
|
|
||||||
LIMIT 1;""",
|
|
||||||
{"user_uuid": user_uuid, "tenant_id": tenant_id})
|
|
||||||
)
|
)
|
||||||
r = cur.fetchone()
|
r = cur.fetchone()
|
||||||
return helper.dict_to_camel_case(r)
|
return helper.dict_to_camel_case(r)
|
||||||
|
|
@ -323,11 +310,11 @@ def get_deleted_by_uuid(user_uuid, tenant_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.tenant_id,
|
users.tenant_id,
|
||||||
email,
|
email,
|
||||||
role,
|
role,
|
||||||
users.name,
|
users.name,
|
||||||
users.data,
|
users.data,
|
||||||
users.internal_id,
|
users.internal_id,
|
||||||
|
|
@ -375,8 +362,8 @@ def __get_account_info(tenant_id, user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT users.name,
|
f"""SELECT users.name,
|
||||||
tenants.name AS tenant_name,
|
tenants.name AS tenant_name,
|
||||||
tenants.opt_out
|
tenants.opt_out
|
||||||
FROM public.users INNER JOIN public.tenants USING (tenant_id)
|
FROM public.users INNER JOIN public.tenants USING (tenant_id)
|
||||||
WHERE users.user_id = %(userId)s
|
WHERE users.user_id = %(userId)s
|
||||||
|
|
@ -457,11 +444,11 @@ def get_by_email_only(email):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.tenant_id,
|
users.tenant_id,
|
||||||
users.email,
|
users.email,
|
||||||
users.role,
|
users.role,
|
||||||
users.name,
|
users.name,
|
||||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
|
|
@ -473,7 +460,7 @@ def get_by_email_only(email):
|
||||||
roles.name AS role_name
|
roles.name AS role_name
|
||||||
FROM public.users LEFT JOIN public.basic_authentication USING(user_id)
|
FROM public.users LEFT JOIN public.basic_authentication USING(user_id)
|
||||||
INNER JOIN public.roles USING(role_id)
|
INNER JOIN public.roles USING(role_id)
|
||||||
WHERE users.email = %(email)s
|
WHERE users.email = %(email)s
|
||||||
AND users.deleted_at IS NULL
|
AND users.deleted_at IS NULL
|
||||||
LIMIT 1;""",
|
LIMIT 1;""",
|
||||||
{"email": email})
|
{"email": email})
|
||||||
|
|
@ -481,50 +468,39 @@ def get_by_email_only(email):
|
||||||
r = cur.fetchone()
|
r = cur.fetchone()
|
||||||
return helper.dict_to_camel_case(r)
|
return helper.dict_to_camel_case(r)
|
||||||
|
|
||||||
def get_users_paginated(start_index, count=None, email=None):
|
def get_users_paginated(start_index, tenant_id, count=None):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
"""
|
||||||
users.user_id AS id,
|
SELECT *
|
||||||
users.tenant_id,
|
FROM public.users
|
||||||
users.email AS email,
|
WHERE
|
||||||
users.data AS data,
|
users.deleted_at IS NULL
|
||||||
users.role,
|
AND users.tenant_id = %(tenant_id)s
|
||||||
users.name AS name,
|
LIMIT %(limit)s
|
||||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
OFFSET %(offset)s;
|
||||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
""",
|
||||||
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
{
|
||||||
origin,
|
"offset": start_index - 1,
|
||||||
basic_authentication.password IS NOT NULL AS has_password,
|
"limit": count,
|
||||||
role_id,
|
"tenant_id": tenant_id
|
||||||
internal_id,
|
},
|
||||||
roles.name AS role_name
|
)
|
||||||
FROM public.users LEFT JOIN public.basic_authentication USING(user_id)
|
|
||||||
INNER JOIN public.roles USING(role_id)
|
|
||||||
WHERE users.deleted_at IS NULL
|
|
||||||
AND users.data ? 'user_id'
|
|
||||||
AND email = COALESCE(%(email)s, email)
|
|
||||||
LIMIT %(count)s
|
|
||||||
OFFSET %(startIndex)s;;""",
|
|
||||||
{"startIndex": start_index - 1, "count": count, "email": email})
|
|
||||||
)
|
)
|
||||||
r = cur.fetchall()
|
r = cur.fetchall()
|
||||||
if len(r):
|
return helper.list_to_camel_case(r)
|
||||||
r = helper.list_to_camel_case(r)
|
|
||||||
return r
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def get_member(tenant_id, user_id):
|
def get_member(tenant_id, user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.email,
|
users.email,
|
||||||
users.role,
|
users.role,
|
||||||
users.name,
|
users.name,
|
||||||
users.created_at,
|
users.created_at,
|
||||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
|
|
@ -535,7 +511,7 @@ def get_member(tenant_id, user_id):
|
||||||
invitation_token,
|
invitation_token,
|
||||||
role_id,
|
role_id,
|
||||||
roles.name AS role_name
|
roles.name AS role_name
|
||||||
FROM public.users
|
FROM public.users
|
||||||
LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
||||||
LEFT JOIN public.roles USING (role_id)
|
LEFT JOIN public.roles USING (role_id)
|
||||||
WHERE users.tenant_id = %(tenant_id)s AND users.deleted_at IS NULL AND users.user_id = %(user_id)s
|
WHERE users.tenant_id = %(tenant_id)s AND users.deleted_at IS NULL AND users.user_id = %(user_id)s
|
||||||
|
|
@ -557,11 +533,11 @@ def get_members(tenant_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.email,
|
users.email,
|
||||||
users.role,
|
users.role,
|
||||||
users.name,
|
users.name,
|
||||||
users.created_at,
|
users.created_at,
|
||||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
|
|
@ -572,10 +548,10 @@ def get_members(tenant_id):
|
||||||
invitation_token,
|
invitation_token,
|
||||||
role_id,
|
role_id,
|
||||||
roles.name AS role_name
|
roles.name AS role_name
|
||||||
FROM public.users
|
FROM public.users
|
||||||
LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
||||||
LEFT JOIN public.roles USING (role_id)
|
LEFT JOIN public.roles USING (role_id)
|
||||||
WHERE users.tenant_id = %(tenant_id)s
|
WHERE users.tenant_id = %(tenant_id)s
|
||||||
AND users.deleted_at IS NULL
|
AND users.deleted_at IS NULL
|
||||||
AND NOT users.service_account
|
AND NOT users.service_account
|
||||||
ORDER BY name, user_id""",
|
ORDER BY name, user_id""",
|
||||||
|
|
@ -614,7 +590,7 @@ def delete_member(user_id, tenant_id, id_to_delete):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(f"""UPDATE public.users
|
cur.mogrify(f"""UPDATE public.users
|
||||||
SET deleted_at = timezone('utc'::text, now()),
|
SET deleted_at = timezone('utc'::text, now()),
|
||||||
jwt_iat= NULL, jwt_refresh_jti= NULL,
|
jwt_iat= NULL, jwt_refresh_jti= NULL,
|
||||||
jwt_refresh_iat= NULL,
|
jwt_refresh_iat= NULL,
|
||||||
role_id=NULL
|
role_id=NULL
|
||||||
WHERE user_id=%(user_id)s AND tenant_id=%(tenant_id)s;""",
|
WHERE user_id=%(user_id)s AND tenant_id=%(tenant_id)s;""",
|
||||||
|
|
@ -634,10 +610,10 @@ def delete_member_as_admin(tenant_id, id_to_delete):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id AS user_id,
|
users.user_id AS user_id,
|
||||||
users.tenant_id,
|
users.tenant_id,
|
||||||
email,
|
email,
|
||||||
role,
|
role,
|
||||||
users.name,
|
users.name,
|
||||||
origin,
|
origin,
|
||||||
|
|
@ -654,12 +630,12 @@ def delete_member_as_admin(tenant_id, id_to_delete):
|
||||||
role = 'owner'
|
role = 'owner'
|
||||||
AND users.tenant_id = %(tenant_id)s
|
AND users.tenant_id = %(tenant_id)s
|
||||||
AND users.deleted_at IS NULL
|
AND users.deleted_at IS NULL
|
||||||
AND (roles.role_id IS NULL OR roles.deleted_at IS NULL AND roles.tenant_id = %(tenant_id)s)
|
AND (roles.role_id IS NULL OR roles.deleted_at IS NULL AND roles.tenant_id = %(tenant_id)s)
|
||||||
LIMIT 1;""",
|
LIMIT 1;""",
|
||||||
{"tenant_id": tenant_id, "user_uuid": id_to_delete})
|
{"tenant_id": tenant_id, "user_uuid": id_to_delete})
|
||||||
)
|
)
|
||||||
r = cur.fetchone()
|
r = cur.fetchone()
|
||||||
|
|
||||||
if r["user_id"] == id_to_delete:
|
if r["user_id"] == id_to_delete:
|
||||||
return {"errors": ["unauthorized, cannot delete self"]}
|
return {"errors": ["unauthorized, cannot delete self"]}
|
||||||
|
|
||||||
|
|
@ -677,7 +653,7 @@ def delete_member_as_admin(tenant_id, id_to_delete):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(f"""UPDATE public.users
|
cur.mogrify(f"""UPDATE public.users
|
||||||
SET deleted_at = timezone('utc'::text, now()),
|
SET deleted_at = timezone('utc'::text, now()),
|
||||||
jwt_iat= NULL, jwt_refresh_jti= NULL,
|
jwt_iat= NULL, jwt_refresh_jti= NULL,
|
||||||
jwt_refresh_iat= NULL,
|
jwt_refresh_iat= NULL,
|
||||||
role_id=NULL
|
role_id=NULL
|
||||||
WHERE user_id=%(user_id)s AND tenant_id=%(tenant_id)s;""",
|
WHERE user_id=%(user_id)s AND tenant_id=%(tenant_id)s;""",
|
||||||
|
|
@ -743,8 +719,8 @@ def email_exists(email):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
count(user_id)
|
count(user_id)
|
||||||
FROM public.users
|
FROM public.users
|
||||||
WHERE
|
WHERE
|
||||||
email = %(email)s
|
email = %(email)s
|
||||||
|
|
@ -760,8 +736,8 @@ def get_deleted_user_by_email(email):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
*
|
*
|
||||||
FROM public.users
|
FROM public.users
|
||||||
WHERE
|
WHERE
|
||||||
email = %(email)s
|
email = %(email)s
|
||||||
|
|
@ -777,7 +753,7 @@ def get_by_invitation_token(token, pass_token=None):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
*,
|
*,
|
||||||
DATE_PART('day',timezone('utc'::text, now()) \
|
DATE_PART('day',timezone('utc'::text, now()) \
|
||||||
- COALESCE(basic_authentication.invited_at,'2000-01-01'::timestamp ))>=1 AS expired_invitation,
|
- COALESCE(basic_authentication.invited_at,'2000-01-01'::timestamp ))>=1 AS expired_invitation,
|
||||||
|
|
@ -797,15 +773,15 @@ def auth_exists(user_id, tenant_id, jwt_iat) -> bool:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT user_id,
|
f"""SELECT user_id,
|
||||||
EXTRACT(epoch FROM jwt_iat)::BIGINT AS jwt_iat,
|
EXTRACT(epoch FROM jwt_iat)::BIGINT AS jwt_iat,
|
||||||
changed_at,
|
changed_at,
|
||||||
service_account,
|
service_account,
|
||||||
basic_authentication.user_id IS NOT NULL AS has_basic_auth
|
basic_authentication.user_id IS NOT NULL AS has_basic_auth
|
||||||
FROM public.users
|
FROM public.users
|
||||||
LEFT JOIN public.basic_authentication USING(user_id)
|
LEFT JOIN public.basic_authentication USING(user_id)
|
||||||
WHERE user_id = %(userId)s
|
WHERE user_id = %(userId)s
|
||||||
AND tenant_id = %(tenant_id)s
|
AND tenant_id = %(tenant_id)s
|
||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
LIMIT 1;""",
|
LIMIT 1;""",
|
||||||
{"userId": user_id, "tenant_id": tenant_id})
|
{"userId": user_id, "tenant_id": tenant_id})
|
||||||
)
|
)
|
||||||
|
|
@ -819,9 +795,9 @@ def auth_exists(user_id, tenant_id, jwt_iat) -> bool:
|
||||||
def refresh_auth_exists(user_id, tenant_id, jwt_jti=None):
|
def refresh_auth_exists(user_id, tenant_id, jwt_jti=None):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(f"""SELECT user_id
|
cur.mogrify(f"""SELECT user_id
|
||||||
FROM public.users
|
FROM public.users
|
||||||
WHERE user_id = %(userId)s
|
WHERE user_id = %(userId)s
|
||||||
AND tenant_id= %(tenant_id)s
|
AND tenant_id= %(tenant_id)s
|
||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
AND jwt_refresh_jti = %(jwt_jti)s
|
AND jwt_refresh_jti = %(jwt_jti)s
|
||||||
|
|
@ -866,17 +842,17 @@ def change_jwt_iat_jti(user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""UPDATE public.users
|
query = cur.mogrify(f"""UPDATE public.users
|
||||||
SET jwt_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
SET jwt_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
||||||
jwt_refresh_jti = 0,
|
jwt_refresh_jti = 0,
|
||||||
jwt_refresh_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
jwt_refresh_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
||||||
spot_jwt_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
spot_jwt_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
||||||
spot_jwt_refresh_jti = 0,
|
spot_jwt_refresh_jti = 0,
|
||||||
spot_jwt_refresh_iat = timezone('utc'::text, now()-INTERVAL '10s')
|
spot_jwt_refresh_iat = timezone('utc'::text, now()-INTERVAL '10s')
|
||||||
WHERE user_id = %(user_id)s
|
WHERE user_id = %(user_id)s
|
||||||
RETURNING EXTRACT (epoch FROM jwt_iat)::BIGINT AS jwt_iat,
|
RETURNING EXTRACT (epoch FROM jwt_iat)::BIGINT AS jwt_iat,
|
||||||
jwt_refresh_jti,
|
jwt_refresh_jti,
|
||||||
EXTRACT (epoch FROM jwt_refresh_iat)::BIGINT AS jwt_refresh_iat,
|
EXTRACT (epoch FROM jwt_refresh_iat)::BIGINT AS jwt_refresh_iat,
|
||||||
EXTRACT (epoch FROM spot_jwt_iat)::BIGINT AS spot_jwt_iat,
|
EXTRACT (epoch FROM spot_jwt_iat)::BIGINT AS spot_jwt_iat,
|
||||||
spot_jwt_refresh_jti,
|
spot_jwt_refresh_jti,
|
||||||
EXTRACT (epoch FROM spot_jwt_refresh_iat)::BIGINT AS spot_jwt_refresh_iat;""",
|
EXTRACT (epoch FROM spot_jwt_refresh_iat)::BIGINT AS spot_jwt_refresh_iat;""",
|
||||||
{"user_id": user_id})
|
{"user_id": user_id})
|
||||||
cur.execute(query)
|
cur.execute(query)
|
||||||
|
|
@ -888,10 +864,10 @@ def refresh_jwt_iat_jti(user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""UPDATE public.users
|
query = cur.mogrify(f"""UPDATE public.users
|
||||||
SET jwt_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
SET jwt_iat = timezone('utc'::text, now()-INTERVAL '10s'),
|
||||||
jwt_refresh_jti = jwt_refresh_jti + 1
|
jwt_refresh_jti = jwt_refresh_jti + 1
|
||||||
WHERE user_id = %(user_id)s
|
WHERE user_id = %(user_id)s
|
||||||
RETURNING EXTRACT (epoch FROM jwt_iat)::BIGINT AS jwt_iat,
|
RETURNING EXTRACT (epoch FROM jwt_iat)::BIGINT AS jwt_iat,
|
||||||
jwt_refresh_jti,
|
jwt_refresh_jti,
|
||||||
EXTRACT (epoch FROM jwt_refresh_iat)::BIGINT AS jwt_refresh_iat;""",
|
EXTRACT (epoch FROM jwt_refresh_iat)::BIGINT AS jwt_refresh_iat;""",
|
||||||
{"user_id": user_id})
|
{"user_id": user_id})
|
||||||
cur.execute(query)
|
cur.execute(query)
|
||||||
|
|
@ -904,7 +880,7 @@ def authenticate(email, password, for_change_password=False) -> dict | bool | No
|
||||||
return {"errors": ["must sign-in with SSO, enforced by admin"]}
|
return {"errors": ["must sign-in with SSO, enforced by admin"]}
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(
|
query = cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.tenant_id,
|
users.tenant_id,
|
||||||
users.role,
|
users.role,
|
||||||
|
|
@ -919,7 +895,7 @@ def authenticate(email, password, for_change_password=False) -> dict | bool | No
|
||||||
users.service_account
|
users.service_account
|
||||||
FROM public.users AS users INNER JOIN public.basic_authentication USING(user_id)
|
FROM public.users AS users INNER JOIN public.basic_authentication USING(user_id)
|
||||||
LEFT JOIN public.roles ON (roles.role_id = users.role_id AND roles.tenant_id = users.tenant_id)
|
LEFT JOIN public.roles ON (roles.role_id = users.role_id AND roles.tenant_id = users.tenant_id)
|
||||||
WHERE users.email = %(email)s
|
WHERE users.email = %(email)s
|
||||||
AND basic_authentication.password = crypt(%(password)s, basic_authentication.password)
|
AND basic_authentication.password = crypt(%(password)s, basic_authentication.password)
|
||||||
AND basic_authentication.user_id = (SELECT su.user_id FROM public.users AS su WHERE su.email=%(email)s AND su.deleted_at IS NULL LIMIT 1)
|
AND basic_authentication.user_id = (SELECT su.user_id FROM public.users AS su WHERE su.email=%(email)s AND su.deleted_at IS NULL LIMIT 1)
|
||||||
AND (roles.role_id IS NULL OR roles.deleted_at IS NULL)
|
AND (roles.role_id IS NULL OR roles.deleted_at IS NULL)
|
||||||
|
|
@ -932,7 +908,7 @@ def authenticate(email, password, for_change_password=False) -> dict | bool | No
|
||||||
query = cur.mogrify(
|
query = cur.mogrify(
|
||||||
f"""SELECT 1
|
f"""SELECT 1
|
||||||
FROM public.users
|
FROM public.users
|
||||||
WHERE users.email = %(email)s
|
WHERE users.email = %(email)s
|
||||||
AND users.deleted_at IS NULL
|
AND users.deleted_at IS NULL
|
||||||
AND users.origin IS NOT NULL
|
AND users.origin IS NOT NULL
|
||||||
LIMIT 1;""",
|
LIMIT 1;""",
|
||||||
|
|
@ -983,17 +959,17 @@ def get_user_role(tenant_id, user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.email,
|
users.email,
|
||||||
users.role,
|
users.role,
|
||||||
users.name,
|
users.name,
|
||||||
users.created_at,
|
users.created_at,
|
||||||
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||||
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||||
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member
|
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member
|
||||||
FROM public.users
|
FROM public.users
|
||||||
WHERE users.deleted_at IS NULL
|
WHERE users.deleted_at IS NULL
|
||||||
AND users.user_id=%(user_id)s
|
AND users.user_id=%(user_id)s
|
||||||
AND users.tenant_id=%(tenant_id)s
|
AND users.tenant_id=%(tenant_id)s
|
||||||
LIMIT 1""",
|
LIMIT 1""",
|
||||||
|
|
@ -1007,7 +983,7 @@ def create_sso_user(tenant_id, email, admin, name, origin, role_id, internal_id=
|
||||||
query = cur.mogrify(f"""\
|
query = cur.mogrify(f"""\
|
||||||
WITH u AS (
|
WITH u AS (
|
||||||
INSERT INTO public.users (tenant_id, email, role, name, data, origin, internal_id, role_id)
|
INSERT INTO public.users (tenant_id, email, role, name, data, origin, internal_id, role_id)
|
||||||
VALUES (%(tenant_id)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s,
|
VALUES (%(tenant_id)s, %(email)s, %(role)s, %(name)s, %(data)s, %(origin)s, %(internal_id)s,
|
||||||
(SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
(SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))))
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))))
|
||||||
|
|
@ -1033,7 +1009,7 @@ def create_sso_user(tenant_id, email, admin, name, origin, role_id, internal_id=
|
||||||
query
|
query
|
||||||
)
|
)
|
||||||
return helper.dict_to_camel_case(cur.fetchone())
|
return helper.dict_to_camel_case(cur.fetchone())
|
||||||
|
|
||||||
def create_scim_user(
|
def create_scim_user(
|
||||||
tenant_id,
|
tenant_id,
|
||||||
user_uuid,
|
user_uuid,
|
||||||
|
|
@ -1094,7 +1070,7 @@ def __hard_delete_user_uuid(user_uuid):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(
|
query = cur.mogrify(
|
||||||
f"""DELETE FROM public.users
|
f"""DELETE FROM public.users
|
||||||
WHERE users.data->>'user_id' = %(user_uuid)s;""", # removed this: AND users.deleted_at IS NOT NULL
|
WHERE users.data->>'user_id' = %(user_uuid)s;""", # removed this: AND users.deleted_at IS NOT NULL
|
||||||
{"user_uuid": user_uuid})
|
{"user_uuid": user_uuid})
|
||||||
cur.execute(query)
|
cur.execute(query)
|
||||||
|
|
||||||
|
|
@ -1124,7 +1100,7 @@ def refresh(user_id: int, tenant_id: int = -1) -> dict:
|
||||||
def authenticate_sso(email: str, internal_id: str):
|
def authenticate_sso(email: str, internal_id: str):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(
|
query = cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
users.user_id,
|
users.user_id,
|
||||||
users.tenant_id,
|
users.tenant_id,
|
||||||
users.role,
|
users.role,
|
||||||
|
|
@ -1173,13 +1149,13 @@ def restore_sso_user(user_id, tenant_id, email, admin, name, origin, role_id, in
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""\
|
query = cur.mogrify(f"""\
|
||||||
WITH u AS (
|
WITH u AS (
|
||||||
UPDATE public.users
|
UPDATE public.users
|
||||||
SET tenant_id= %(tenant_id)s,
|
SET tenant_id= %(tenant_id)s,
|
||||||
role= %(role)s,
|
role= %(role)s,
|
||||||
name= %(name)s,
|
name= %(name)s,
|
||||||
data= %(data)s,
|
data= %(data)s,
|
||||||
origin= %(origin)s,
|
origin= %(origin)s,
|
||||||
internal_id= %(internal_id)s,
|
internal_id= %(internal_id)s,
|
||||||
role_id= (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
role_id= (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))),
|
||||||
|
|
@ -1198,7 +1174,7 @@ def restore_sso_user(user_id, tenant_id, email, admin, name, origin, role_id, in
|
||||||
invited_at= default,
|
invited_at= default,
|
||||||
change_pwd_token= default,
|
change_pwd_token= default,
|
||||||
change_pwd_expire_at= default,
|
change_pwd_expire_at= default,
|
||||||
changed_at= NULL
|
changed_at= NULL
|
||||||
WHERE user_id = %(user_id)s
|
WHERE user_id = %(user_id)s
|
||||||
RETURNING user_id
|
RETURNING user_id
|
||||||
)
|
)
|
||||||
|
|
@ -1237,13 +1213,13 @@ def restore_scim_user(
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
query = cur.mogrify(f"""\
|
query = cur.mogrify(f"""\
|
||||||
WITH u AS (
|
WITH u AS (
|
||||||
UPDATE public.users
|
UPDATE public.users
|
||||||
SET tenant_id= %(tenant_id)s,
|
SET tenant_id= %(tenant_id)s,
|
||||||
role= %(role)s,
|
role= %(role)s,
|
||||||
name= %(name)s,
|
name= %(name)s,
|
||||||
data= %(data)s,
|
data= %(data)s,
|
||||||
origin= %(origin)s,
|
origin= %(origin)s,
|
||||||
internal_id= %(internal_id)s,
|
internal_id= %(internal_id)s,
|
||||||
role_id= (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
role_id= (SELECT COALESCE((SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND role_id = %(role_id)s),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name = 'Member' LIMIT 1),
|
||||||
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))),
|
(SELECT role_id FROM roles WHERE tenant_id = %(tenant_id)s AND name != 'Owner' LIMIT 1))),
|
||||||
|
|
@ -1262,7 +1238,7 @@ def restore_scim_user(
|
||||||
invited_at= default,
|
invited_at= default,
|
||||||
change_pwd_token= default,
|
change_pwd_token= default,
|
||||||
change_pwd_expire_at= default,
|
change_pwd_expire_at= default,
|
||||||
changed_at= NULL
|
changed_at= NULL
|
||||||
WHERE user_id = %(user_id)s
|
WHERE user_id = %(user_id)s
|
||||||
RETURNING user_id
|
RETURNING user_id
|
||||||
)
|
)
|
||||||
|
|
@ -1290,10 +1266,10 @@ def get_user_settings(user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
settings
|
settings
|
||||||
FROM public.users
|
FROM public.users
|
||||||
WHERE users.deleted_at IS NULL
|
WHERE users.deleted_at IS NULL
|
||||||
AND users.user_id=%(user_id)s
|
AND users.user_id=%(user_id)s
|
||||||
LIMIT 1""",
|
LIMIT 1""",
|
||||||
{"user_id": user_id})
|
{"user_id": user_id})
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Any, Literal, Optional
|
||||||
import copy
|
import copy
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from fastapi import Depends, HTTPException, Header, Query, Response, Request
|
from fastapi import Depends, HTTPException, Header, Query, Response, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_serializer
|
||||||
|
|
||||||
import schemas
|
import schemas
|
||||||
from chalicelib.core import users, roles, tenants
|
from chalicelib.core import users, roles, tenants
|
||||||
from chalicelib.utils.scim_auth import auth_optional, auth_required, create_tokens, verify_refresh_token
|
from chalicelib.utils.scim_auth import auth_optional, auth_required, create_tokens, verify_refresh_token
|
||||||
from routers.base import get_routers
|
from routers.base import get_routers
|
||||||
from routers.scim_constants import RESOURCE_TYPES, SCHEMAS, SERVICE_PROVIDER_CONFIG
|
from routers.scim_constants import RESOURCE_TYPES, SCHEMAS, SERVICE_PROVIDER_CONFIG
|
||||||
|
from routers import scim_helpers
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -189,90 +191,124 @@ class UserRequest(BaseModel):
|
||||||
password: str = Field(default=None)
|
password: str = Field(default=None)
|
||||||
active: bool
|
active: bool
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
|
||||||
schemas: list[str]
|
|
||||||
id: str
|
|
||||||
userName: str
|
|
||||||
name: Name
|
|
||||||
emails: list[Email] # ignore for now
|
|
||||||
displayName: str
|
|
||||||
locale: str
|
|
||||||
externalId: str
|
|
||||||
active: bool
|
|
||||||
groups: list[dict]
|
|
||||||
meta: dict = Field(default={"resourceType": "User"})
|
|
||||||
|
|
||||||
class PatchUserRequest(BaseModel):
|
class PatchUserRequest(BaseModel):
|
||||||
schemas: list[str]
|
schemas: list[str]
|
||||||
Operations: list[dict]
|
Operations: list[dict]
|
||||||
|
|
||||||
|
|
||||||
@public_app.get("/Users", dependencies=[Depends(auth_required)])
|
class ResourceMetaResponse(BaseModel):
|
||||||
async def get_users(
|
resourceType: Literal["ServiceProviderConfig", "ResourceType", "Schema", "User"] | None = None
|
||||||
start_index: int = Query(1, alias="startIndex"),
|
created: datetime | None = None
|
||||||
count: Optional[int] = Query(None, alias="count"),
|
lastModified: datetime | None = None
|
||||||
email: Optional[str] = Query(None, alias="filter"),
|
location: str | None = None
|
||||||
):
|
version: str | None = None
|
||||||
"""Get SCIM Users"""
|
|
||||||
if email:
|
|
||||||
email = email.split(" ")[2].strip('"')
|
|
||||||
result_users = users.get_users_paginated(start_index, count, email)
|
|
||||||
|
|
||||||
serialized_users = []
|
@field_serializer("created", "lastModified")
|
||||||
for user in result_users:
|
def serialize_datetime(self, dt: datetime) -> str | None:
|
||||||
serialized_users.append(
|
if not dt:
|
||||||
UserResponse(
|
return None
|
||||||
schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
id = user["data"]["userId"],
|
|
||||||
userName = user["email"],
|
|
||||||
name = Name.model_validate(user["data"]["name"]),
|
class CommonResourceResponse(BaseModel):
|
||||||
emails = [Email.model_validate(user["data"]["emails"])],
|
id: str
|
||||||
displayName = user["name"],
|
externalId: str | None = None
|
||||||
locale = user["data"]["locale"],
|
schemas: list[
|
||||||
externalId = user["internalId"],
|
Literal[
|
||||||
active = True, # ignore for now, since, can't insert actual timestamp
|
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
|
||||||
groups = [], # ignore
|
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
|
||||||
).model_dump(mode='json')
|
"urn:ietf:params:scim:schemas:core:2.0:Schema",
|
||||||
)
|
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
meta: ResourceMetaResponse | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(CommonResourceResponse):
|
||||||
|
schemas: list[Literal["urn:ietf:params:scim:schemas:core:2.0:User"]] = ["urn:ietf:params:scim:schemas:core:2.0:User"]
|
||||||
|
userName: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class QueryResourceResponse(BaseModel):
|
||||||
|
schemas: list[Literal["urn:ietf:params:scim:api:messages:2.0:ListResponse"]] = ["urn:ietf:params:scim:api:messages:2.0:ListResponse"]
|
||||||
|
totalResults: int
|
||||||
|
# todo(jon): add the other schemas
|
||||||
|
Resources: list[UserResponse]
|
||||||
|
startIndex: int
|
||||||
|
itemsPerPage: int
|
||||||
|
|
||||||
|
|
||||||
|
MAX_USERS_PER_PAGE = 10
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_db_user_to_scim_user(db_user: dict[str, Any], attributes: list[str] | None = None, excluded_attributes: list[str] | None = None) -> UserResponse:
|
||||||
|
user_schema = SCHEMA_IDS_TO_SCHEMA_DETAILS["urn:ietf:params:scim:schemas:core:2.0:User"]
|
||||||
|
all_attributes = scim_helpers.get_all_attribute_names(user_schema)
|
||||||
|
attributes = attributes or all_attributes
|
||||||
|
always_returned_attributes = scim_helpers.get_all_attribute_names_where_returned_is_always(user_schema)
|
||||||
|
included_attributes = list(set(attributes).union(set(always_returned_attributes)))
|
||||||
|
excluded_attributes = excluded_attributes or []
|
||||||
|
excluded_attributes = list(set(excluded_attributes).difference(set(always_returned_attributes)))
|
||||||
|
scim_user = {
|
||||||
|
"id": str(db_user["userId"]),
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "User",
|
||||||
|
"created": db_user["createdAt"],
|
||||||
|
"lastModified": db_user["createdAt"], # todo(jon): we currently don't keep track of this in the db
|
||||||
|
"location": f"Users/{db_user['userId']}"
|
||||||
|
},
|
||||||
|
"userName": db_user["email"],
|
||||||
|
}
|
||||||
|
scim_user = scim_helpers.filter_attributes(scim_user, included_attributes)
|
||||||
|
scim_user = scim_helpers.exclude_attributes(scim_user, excluded_attributes)
|
||||||
|
return UserResponse(**scim_user)
|
||||||
|
|
||||||
|
|
||||||
|
@public_app.get("/Users")
|
||||||
|
async def get_users(
|
||||||
|
tenant_id = Depends(auth_required),
|
||||||
|
requested_start_index: int = Query(1, alias="startIndex"),
|
||||||
|
requested_items_per_page: int | None = Query(None, alias="count"),
|
||||||
|
attributes: list[str] | None = Query(None),
|
||||||
|
excluded_attributes: list[str] | None = Query(None, alias="excludedAttributes"),
|
||||||
|
):
|
||||||
|
start_index = max(1, requested_start_index)
|
||||||
|
items_per_page = min(max(0, requested_items_per_page or MAX_USERS_PER_PAGE), MAX_USERS_PER_PAGE)
|
||||||
|
# todo(jon): this might not be the most efficient thing to do. could be better to just do a count.
|
||||||
|
# but this is the fastest thing at the moment just to test that it's working
|
||||||
|
total_users = users.get_users_paginated(1, tenant_id)
|
||||||
|
db_users = users.get_users_paginated(start_index, tenant_id, count=items_per_page)
|
||||||
|
scim_users = [
|
||||||
|
_convert_db_user_to_scim_user(user, attributes, excluded_attributes)
|
||||||
|
for user in db_users
|
||||||
|
]
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=200,
|
status_code=200,
|
||||||
content={
|
content=QueryResourceResponse(
|
||||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
totalResults=len(total_users),
|
||||||
"totalResults": len(serialized_users),
|
startIndex=start_index,
|
||||||
"startIndex": start_index,
|
itemsPerPage=len(scim_users),
|
||||||
"itemsPerPage": len(serialized_users),
|
Resources=scim_users,
|
||||||
"Resources": serialized_users,
|
).model_dump(mode="json", exclude_none=True),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@public_app.get("/Users/{user_id}", dependencies=[Depends(auth_required)])
|
|
||||||
def get_user(user_id: str):
|
|
||||||
"""Get SCIM User"""
|
|
||||||
tenant_id = 1
|
|
||||||
user = users.get_by_uuid(user_id, tenant_id)
|
|
||||||
if not user:
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=404,
|
|
||||||
content={
|
|
||||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
||||||
"detail": "User not found",
|
|
||||||
"status": 404,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
res = UserResponse(
|
@public_app.get("/Users/{user_id}")
|
||||||
schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
def get_user(
|
||||||
id = user["data"]["userId"],
|
user_id: str,
|
||||||
userName = user["email"],
|
tenant_id = Depends(auth_required),
|
||||||
name = Name.model_validate(user["data"]["name"]),
|
attributes: list[str] | None = Query(None),
|
||||||
emails = [Email.model_validate(user["data"]["emails"])],
|
excluded_attributes: list[str] | None = Query(None, alias="excludedAttributes"),
|
||||||
displayName = user["name"],
|
):
|
||||||
locale = user["data"]["locale"],
|
db_user = users.get_by_uuid(user_id, tenant_id)
|
||||||
externalId = user["internalId"],
|
if not db_user:
|
||||||
active = True, # ignore for now, since, can't insert actual timestamp
|
return _not_found_error_response(user_id)
|
||||||
groups = [], # ignore
|
scim_user = _convert_db_user_to_scim_user(db_user, attributes, excluded_attributes)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=200,
|
||||||
|
content=scim_user.model_dump(mode="json", exclude_none=True)
|
||||||
)
|
)
|
||||||
return JSONResponse(status_code=201, content=res.model_dump(mode='json'))
|
|
||||||
|
|
||||||
|
|
||||||
@public_app.post("/Users", dependencies=[Depends(auth_required)])
|
@public_app.post("/Users", dependencies=[Depends(auth_required)])
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# note(jon): please see https://datatracker.ietf.org/doc/html/rfc7643 for details on these constants
|
# note(jon): please see https://datatracker.ietf.org/doc/html/rfc7643 for details on these constants
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
|
||||||
def _attribute_characteristics(
|
def _attribute_characteristics(
|
||||||
name: str,
|
name: str,
|
||||||
|
|
@ -102,12 +103,12 @@ def _common_resource_attributes(id_required: bool=True, id_uniqueness: str="none
|
||||||
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
|
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
|
||||||
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
|
"urn:ietf:params:scim:schemas:core:2.0:ResourceType",
|
||||||
"urn:ietf:params:scim:schemas:core:2.0:Schema",
|
"urn:ietf:params:scim:schemas:core:2.0:Schema",
|
||||||
# todo(jon): add the user and group schem when completed
|
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
],
|
],
|
||||||
case_exact=True,
|
case_exact=True,
|
||||||
mutability="readOnly",
|
mutability="readOnly",
|
||||||
returned="default",
|
|
||||||
required=True,
|
required=True,
|
||||||
|
returned="always",
|
||||||
),
|
),
|
||||||
_attribute_characteristics(
|
_attribute_characteristics(
|
||||||
name="meta",
|
name="meta",
|
||||||
|
|
@ -670,13 +671,38 @@ SCHEMA_SCHEMA = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
USER_SCHEMA = {
|
||||||
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
|
||||||
|
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
"name": "User",
|
||||||
|
"description": "User account.",
|
||||||
|
"meta": {
|
||||||
|
"resourceType": "Schema",
|
||||||
|
"created": "2025-04-16T14:48:00Z",
|
||||||
|
# note(jon): we might want to think about adding this resource as part of our db
|
||||||
|
# and then updating these timestamps from an api and such. for now, if we update
|
||||||
|
# the configuration, we should update the timestamp here.
|
||||||
|
"lastModified": "2025-04-16T14:48:00Z",
|
||||||
|
"location": "Schemas/urn:ietf:params:scim:schemas:core:2.0:User",
|
||||||
|
},
|
||||||
|
"attributes": [
|
||||||
|
*_common_resource_attributes(),
|
||||||
|
_attribute_characteristics(
|
||||||
|
name="userName",
|
||||||
|
description="A service provider's unique identifier for the user.",
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SCHEMAS = sorted(
|
SCHEMAS = sorted(
|
||||||
# todo(jon): add the user schema
|
|
||||||
[
|
[
|
||||||
SERVICE_PROVIDER_CONFIG_SCHEMA,
|
SERVICE_PROVIDER_CONFIG_SCHEMA,
|
||||||
RESOURCE_TYPE_SCHEMA,
|
RESOURCE_TYPE_SCHEMA,
|
||||||
SCHEMA_SCHEMA,
|
SCHEMA_SCHEMA,
|
||||||
|
USER_SCHEMA,
|
||||||
],
|
],
|
||||||
key=lambda x: x["id"],
|
key=lambda x: x["id"],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
105
ee/api/routers/scim_helpers.py
Normal file
105
ee/api/routers/scim_helpers.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
from typing import Any
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_attribute_names(schema: dict[str, Any]) -> list[str]:
|
||||||
|
result = []
|
||||||
|
def _walk(attrs, prefix=None):
|
||||||
|
for attr in attrs:
|
||||||
|
name = attr["name"]
|
||||||
|
path = f"{prefix}.{name}" if prefix else name
|
||||||
|
result.append(path)
|
||||||
|
if attr["type"] == "complex":
|
||||||
|
sub = attr.get("subAttributes") or attr.get("attributes") or []
|
||||||
|
_walk(sub, path)
|
||||||
|
_walk(schema["attributes"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_attribute_names_where_returned_is_always(schema: dict[str, Any]) -> list[str]:
|
||||||
|
result = []
|
||||||
|
def _walk(attrs, prefix=None):
|
||||||
|
for attr in attrs:
|
||||||
|
name = attr["name"]
|
||||||
|
path = f"{prefix}.{name}" if prefix else name
|
||||||
|
if attr["returned"] == "always":
|
||||||
|
result.append(path)
|
||||||
|
if attr["type"] == "complex":
|
||||||
|
sub = attr.get("subAttributes") or attr.get("attributes") or []
|
||||||
|
_walk(sub, path)
|
||||||
|
_walk(schema["attributes"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def filter_attributes(resource: dict[str, Any], include_list: list[str]) -> dict[str, Any]:
|
||||||
|
result = {}
|
||||||
|
for attr in include_list:
|
||||||
|
parts = attr.split(".", 1)
|
||||||
|
key = parts[0]
|
||||||
|
if key not in resource:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(parts) == 1:
|
||||||
|
# top‑level attr
|
||||||
|
result[key] = resource[key]
|
||||||
|
else:
|
||||||
|
# nested attr
|
||||||
|
sub = resource[key]
|
||||||
|
rest = parts[1]
|
||||||
|
if isinstance(sub, dict):
|
||||||
|
filtered = filter_attributes(sub, [rest])
|
||||||
|
if filtered:
|
||||||
|
result.setdefault(key, {}).update(filtered)
|
||||||
|
elif isinstance(sub, list):
|
||||||
|
# apply to each element
|
||||||
|
new_list = []
|
||||||
|
for item in sub:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
f = filter_attributes(item, [rest])
|
||||||
|
if f:
|
||||||
|
new_list.append(f)
|
||||||
|
if new_list:
|
||||||
|
result[key] = new_list
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def exclude_attributes(resource: dict[str, Any], exclude_list: list[str]) -> dict[str, Any]:
|
||||||
|
exclude_map = {}
|
||||||
|
for attr in exclude_list:
|
||||||
|
parts = attr.split(".", 1)
|
||||||
|
key = parts[0]
|
||||||
|
# rest is empty string for top-level exclusion
|
||||||
|
rest = parts[1] if len(parts) == 2 else ""
|
||||||
|
exclude_map.setdefault(key, []).append(rest)
|
||||||
|
|
||||||
|
new_resource = {}
|
||||||
|
for key, value in resource.items():
|
||||||
|
if key in exclude_map:
|
||||||
|
subs = exclude_map[key]
|
||||||
|
# If any attr has no rest, exclude entire key
|
||||||
|
if "" in subs:
|
||||||
|
continue
|
||||||
|
# Exclude nested attributes
|
||||||
|
if isinstance(value, dict):
|
||||||
|
new_sub = exclude_attributes(value, subs)
|
||||||
|
if not new_sub:
|
||||||
|
continue
|
||||||
|
new_resource[key] = new_sub
|
||||||
|
elif isinstance(value, list):
|
||||||
|
new_list = []
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
new_item = exclude_attributes(item, subs)
|
||||||
|
new_list.append(new_item)
|
||||||
|
else:
|
||||||
|
new_list.append(item)
|
||||||
|
new_resource[key] = new_list
|
||||||
|
else:
|
||||||
|
new_resource[key] = value
|
||||||
|
else:
|
||||||
|
# No exclusion for this key: copy safely
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
new_resource[key] = deepcopy(value)
|
||||||
|
else:
|
||||||
|
new_resource[key] = value
|
||||||
|
return new_resource
|
||||||
Loading…
Add table
Reference in a new issue