From 0de1c361ba3d27aaf44342c35bbb89d9169a5be4 Mon Sep 17 00:00:00 2001 From: Kraiem Taha Yassine Date: Fri, 16 Aug 2024 18:35:05 +0200 Subject: [PATCH] Dev (#2496) * fix(chalice): fixed Math-operators validation refactor(chalice): search for sessions that have events for heatmaps * refactor(chalice): search for sessions that have at least 1 location event for heatmaps * fix(chalice): fixed Math-operators validation refactor(chalice): search for sessions that have events for heatmaps * refactor(chalice): search for sessions that have at least 1 location event for heatmaps * feat(chalice): autocomplete return top 10 with stats * fix(chalice): fixed autocomplete top 10 meta-filters * feat(chalice): Update user on SSO refactor(chalice): refactored SSO --- ee/api/chalicelib/core/users.py | 8 ++- ee/api/chalicelib/utils/SAML2_helper.py | 6 +- ee/api/routers/saml.py | 90 ++++++++++++++++--------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/ee/api/chalicelib/core/users.py b/ee/api/chalicelib/core/users.py index 35afa6576..f282e1d8d 100644 --- a/ee/api/chalicelib/core/users.py +++ b/ee/api/chalicelib/core/users.py @@ -379,8 +379,12 @@ def get_by_email_only(email): (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, - basic_authentication.password IS NOT NULL AS has_password - FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id + basic_authentication.password IS NOT NULL AS has_password, + role_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.email = %(email)s AND users.deleted_at IS NULL LIMIT 1;""", diff --git a/ee/api/chalicelib/utils/SAML2_helper.py b/ee/api/chalicelib/utils/SAML2_helper.py index 4857dda6f..c8431208a 100644 --- a/ee/api/chalicelib/utils/SAML2_helper.py +++ b/ee/api/chalicelib/utils/SAML2_helper.py @@ -11,7 +11,11 @@ from starlette.datastructures import FormData if config("ENABLE_SSO", cast=bool, default=True): from onelogin.saml2.auth import OneLogin_Saml2_Auth -API_PREFIX = "/api" +if config("LOCAL_DEV", default=False, cast=bool): + API_PREFIX = "" +else: + API_PREFIX = "/api" + SAML2 = { "strict": config("saml_strict", cast=bool, default=True), "debug": config("saml_debug", cast=bool, default=True), diff --git a/ee/api/routers/saml.py b/ee/api/routers/saml.py index 9584b24e2..b58d617c1 100644 --- a/ee/api/routers/saml.py +++ b/ee/api/routers/saml.py @@ -1,8 +1,12 @@ import json import logging +from decouple import config from fastapi import HTTPException, Request, Response, status +from onelogin.saml2.auth import OneLogin_Saml2_Logout_Request +from starlette.responses import RedirectResponse +from chalicelib.core import users, tenants, roles from chalicelib.utils import SAML2_helper from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth from routers.base import get_routers @@ -10,12 +14,6 @@ from routers.base import get_routers logger = logging.getLogger(__name__) public_app, app, app_apikey = get_routers() -from decouple import config - -from onelogin.saml2.auth import OneLogin_Saml2_Logout_Request - -from chalicelib.core import users, tenants, roles -from starlette.responses import RedirectResponse @public_app.get("/sso/saml2", tags=["saml2"]) @@ -28,7 +26,7 @@ async def start_sso(request: Request, iFrame: bool = False, spot: bool = False): return RedirectResponse(url=sso_built_url) -async def __process_assertion(request: Request, tenant_key=None): +async def __process_assertion(request: Request, tenant_key=None) -> Response | dict: req = await prepare_request(request=request) session = req["cookie"]["session"] auth = init_saml_auth(req) @@ -91,45 +89,71 @@ async def __process_assertion(request: Request, tenant_key=None): if t is None: logger.error("invalid tenantKey, please copy the correct value from Preferences > Account") return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]} - existing = users.get_by_email_only(auth.get_nameid()) + existing = users.get_by_email_only(email) + role_names = user_data.get("role", []) + if len(role_names) == 0: + logger.info("No role specified, setting role to member") + role_names = ["member"] + role = None + for r in role_names: + if r.lower() == existing["roleName"].lower(): + role = {"roleId": existing["roleId"], "name": r} + else: + role = roles.get_role_by_name(tenant_id=t['tenantId'], name=r) + + if role is not None: + break + + if role is None: + return {"errors": [f"role '{role_names}' not found, please create it in OpenReplay first"]} + logger.info(f"received roles:{role_names}; using:{role['name']}") + admin_privileges = user_data.get("adminPrivileges", []) + admin_privileges = not (len(admin_privileges) == 0 + or admin_privileges[0] is None + or admin_privileges[0].lower() == "false") + internal_id = next(iter(user_data.get("internalId", [])), None) - + full_name = " ".join(user_data.get("firstName", []) + user_data.get("lastName", [])) if existing is None: - role_name = user_data.get("role", []) - if len(role_name) == 0: - logger.info("No role specified, setting role to member") - role_name = ["member"] - role_name = role_name[0] - role = roles.get_role_by_name(tenant_id=t['tenantId'], name=role_name) - if role is None: - return {"errors": [f"role {role_name} not found, please create it in openreplay first"]} - - admin_privileges = user_data.get("adminPrivileges", []) - admin_privileges = not (len(admin_privileges) == 0 - or admin_privileges[0] is None - or admin_privileges[0].lower() == "false") - deleted = users.get_deleted_user_by_email(auth.get_nameid()) if deleted is not None: logger.info("== restore deleted user ==") users.restore_sso_user(user_id=deleted["userId"], tenant_id=t['tenantId'], email=email, admin=admin_privileges, origin=SAML2_helper.get_saml2_provider(), - name=" ".join(user_data.get("firstName", []) + user_data.get("lastName", [])), - internal_id=internal_id, role_id=role["roleId"]) + name=full_name, internal_id=internal_id, role_id=role["roleId"]) else: logger.info("== new user ==") users.create_sso_user(tenant_id=t['tenantId'], email=email, admin=admin_privileges, origin=SAML2_helper.get_saml2_provider(), - name=" ".join(user_data.get("firstName", []) + user_data.get("lastName", [])), - internal_id=internal_id, role_id=role["roleId"]) + name=full_name, internal_id=internal_id, role_id=role["roleId"]) else: if t['tenantId'] != existing["tenantId"]: logger.warning("user exists for a different tenant") return {"errors": ["user exists for a different tenant"]} - if existing.get("origin") is None: - logger.info(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==") - users.update(tenant_id=t['tenantId'], user_id=existing["userId"], - changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id}) + # Check difference between existing user and received data + received_data = { + "role": "admin" if admin_privileges else "member", + "origin": SAML2_helper.get_saml2_provider(), + "name": full_name, + "internal_id": internal_id, + "role_id": role["roleId"] + } + existing_data = { + "role": "admin" if existing["admin"] else "member", + "origin": existing["origin"], + "name": existing["name"], + "internal_id": existing["internalId"], + "role_id": existing["roleId"] + } + to_update = {} + for k in existing_data.keys(): + if (k != "role" or not existing["superAdmin"]) and existing_data[k] != received_data[k]: + to_update[k] = received_data[k] + + if len(to_update.keys()) > 0: + logger.info(f"== Updating user:{existing['userId']}: {to_update} ==") + users.update(tenant_id=t['tenantId'], user_id=existing["userId"], changes=to_update) + jwt = users.authenticate_sso(email=email, internal_id=internal_id, include_spot=spot) if jwt is None: return {"errors": ["null JWT"]} @@ -150,13 +174,13 @@ async def __process_assertion(request: Request, tenant_key=None): @public_app.post('/sso/saml2/acs', tags=["saml2"]) @public_app.post('/sso/saml2/acs/', tags=["saml2"]) async def process_sso_assertion(request: Request): - return await __process_assertion(request) + return await __process_assertion(request=request) @public_app.post('/sso/saml2/acs/{tenantKey}', tags=["saml2"]) @public_app.post('/sso/saml2/acs/{tenantKey}/', tags=["saml2"]) async def process_sso_assertion_tk(tenantKey: str, request: Request): - return await __process_assertion(request, tenantKey) + return await __process_assertion(request=request, tenant_key=tenantKey) @public_app.get('/sso/saml2/sls', tags=["saml2"])