diff --git a/api/chalicelib/core/alerts.py b/api/chalicelib/core/alerts.py index dfa86ed75..fae4e4883 100644 --- a/api/chalicelib/core/alerts.py +++ b/api/chalicelib/core/alerts.py @@ -9,7 +9,7 @@ import schemas from chalicelib.core import notifications, webhook from chalicelib.core.collaboration_msteams import MSTeams from chalicelib.core.collaboration_slack import Slack -from chalicelib.utils import pg_client, helper, email_helper +from chalicelib.utils import pg_client, helper, email_helper, smtp from chalicelib.utils.TimeUTC import TimeUTC @@ -157,7 +157,7 @@ def send_by_email(notification, destination): def send_by_email_batch(notifications_list): - if not helper.has_smtp(): + if not smtp.has_smtp(): logging.info("no SMTP configuration for email notifications") if notifications_list is None or len(notifications_list) == 0: logging.info("no email notifications") diff --git a/api/chalicelib/core/reset_password.py b/api/chalicelib/core/reset_password.py index 782b63ad4..0637baa15 100644 --- a/api/chalicelib/core/reset_password.py +++ b/api/chalicelib/core/reset_password.py @@ -1,6 +1,6 @@ import schemas from chalicelib.core import users -from chalicelib.utils import email_helper, captcha, helper +from chalicelib.utils import email_helper, captcha, helper, smtp def reset(data: schemas.ForgetPasswordPayloadSchema): @@ -8,7 +8,7 @@ def reset(data: schemas.ForgetPasswordPayloadSchema): if helper.allow_captcha() and not captcha.is_valid(data.g_recaptcha_response): print("error: Invalid captcha.") return {"errors": ["Invalid captcha."]} - if not helper.has_smtp(): + if not smtp.has_smtp(): return {"errors": ["no SMTP configuration found, you can ask your admin to reset your password"]} a_users = users.get_by_email_only(data.email) if a_users: diff --git a/api/chalicelib/core/users.py b/api/chalicelib/core/users.py index 3e92c8a56..3bc17d860 100644 --- a/api/chalicelib/core/users.py +++ b/api/chalicelib/core/users.py @@ -7,7 +7,7 @@ from fastapi import BackgroundTasks import schemas from chalicelib.core import authorizers, metadata, projects from chalicelib.core import tenants, assist -from chalicelib.utils import email_helper +from chalicelib.utils import email_helper, smtp from chalicelib.utils import helper from chalicelib.utils import pg_client from chalicelib.utils.TimeUTC import TimeUTC @@ -501,7 +501,7 @@ def set_password_invitation(user_id, new_password): c = tenants.get_by_tenant_id(tenant_id) c.pop("createdAt") c["projects"] = projects.get_projects(tenant_id=tenant_id, recorded=True) - c["smtp"] = helper.has_smtp() + c["smtp"] = smtp.has_smtp() c["iceServers"] = assist.get_ice_servers() return { 'jwt': r.pop('jwt'), diff --git a/api/chalicelib/core/weekly_report.py b/api/chalicelib/core/weekly_report.py index afb0843ce..9db77c30b 100644 --- a/api/chalicelib/core/weekly_report.py +++ b/api/chalicelib/core/weekly_report.py @@ -1,4 +1,4 @@ -from chalicelib.utils import pg_client, helper, email_helper +from chalicelib.utils import pg_client, helper, email_helper, smtp from chalicelib.utils.TimeUTC import TimeUTC from chalicelib.utils.helper import get_issue_title @@ -29,7 +29,7 @@ def edit_config(user_id, weekly_report): def cron(): - if not helper.has_smtp(): + if not smtp.has_smtp(): print("!!! No SMTP configuration found, ignoring weekly report") return _now = TimeUTC.now() diff --git a/api/chalicelib/utils/__init__.py b/api/chalicelib/utils/__init__.py index 0744a56a2..d9987f92c 100644 --- a/api/chalicelib/utils/__init__.py +++ b/api/chalicelib/utils/__init__.py @@ -1,10 +1,10 @@ -from . import helper +from . import smtp import logging from decouple import config logging.basicConfig(level=config("LOGLEVEL", default=logging.INFO)) -if helper.has_smtp(): +if smtp.has_smtp(): logging.info("valid SMTP configuration found") else: logging.info("no SMTP configuration found or SMTP validation failed") diff --git a/api/chalicelib/utils/helper.py b/api/chalicelib/utils/helper.py index 1afc2e39e..41408088b 100644 --- a/api/chalicelib/utils/helper.py +++ b/api/chalicelib/utils/helper.py @@ -266,27 +266,6 @@ def __decimal_limit(value, limit): return value / factor -VALID_SMTP = None -SMTP_ERROR = None - - -def has_smtp(): - global VALID_SMTP, SMTP_ERROR - if SMTP_ERROR is not None: - logging.error("!!! SMTP error found, disabling SMTP configuration:") - logging.error(SMTP_ERROR) - - if VALID_SMTP is not None: - return VALID_SMTP - - if config("EMAIL_HOST") is not None and len(config("EMAIL_HOST")) > 0: - VALID_SMTP, SMTP_ERROR = smtp.check_connexion() - return VALID_SMTP - else: - logging.info("no SMTP configuration found") - return False - - def old_search_payload_to_flat(values): # in case the old search body was passed if values.get("events") is not None: diff --git a/api/chalicelib/utils/smtp.py b/api/chalicelib/utils/smtp.py index 03e812a1f..4182d5a10 100644 --- a/api/chalicelib/utils/smtp.py +++ b/api/chalicelib/utils/smtp.py @@ -70,6 +70,27 @@ class SMTPClient: return True, None +VALID_SMTP = None +SMTP_ERROR = None + + +def has_smtp(): + global VALID_SMTP, SMTP_ERROR + if SMTP_ERROR is not None: + logging.error("!!! SMTP error found, disabling SMTP configuration:") + logging.error(SMTP_ERROR) + + if VALID_SMTP is not None: + return VALID_SMTP + + if config("EMAIL_HOST") is not None and len(config("EMAIL_HOST")) > 0: + VALID_SMTP, SMTP_ERROR = check_connexion() + return VALID_SMTP + else: + logging.info("no SMTP configuration found") + return False + + def check_connexion(): # check SMTP host&port import socket diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index 2300e59f8..86c8c87a9 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -12,7 +12,7 @@ from chalicelib.core import sessions_viewed from chalicelib.core import tenants, users, projects, license from chalicelib.core import webhook from chalicelib.core.collaboration_slack import Slack -from chalicelib.utils import captcha +from chalicelib.utils import captcha, smtp from chalicelib.utils import helper from chalicelib.utils.TimeUTC import TimeUTC from or_dependencies import OR_context @@ -57,7 +57,7 @@ async def login_user(data: schemas.UserLoginSchema = Body(...)): detail=r["errors"][0] ) - r["smtp"] = helper.has_smtp() + r["smtp"] = smtp.has_smtp() content = { 'jwt': r.pop('jwt'), 'data': { @@ -85,7 +85,7 @@ async def get_account(context: schemas.CurrentContext = Depends(OR_context)): **r, **t, **license.get_status(context.tenant_id), - "smtp": helper.has_smtp(), + "smtp": smtp.has_smtp(), # "iceServers": assist.get_ice_servers() } } diff --git a/ee/api/Pipfile b/ee/api/Pipfile index ceab80dcb..9ca30c1c5 100644 --- a/ee/api/Pipfile +++ b/ee/api/Pipfile @@ -12,15 +12,14 @@ psycopg2-binary = "==2.9.6" elasticsearch = "==8.8.0" jira = "==3.5.1" fastapi = "==0.96.0" +uvicorn = {version = "==0.22.0", extras = ["standard"]} python-decouple = "==3.8" +pydantic = {version = "==1.10.8", extras = ["email"]} apscheduler = "==3.10.1" -python3-saml = "==1.15.0" +clickhouse-driver = {version = "==0.2.5", extras = ["lz4"]} python-multipart = "==0.0.6" redis = "==4.5.5" azure-storage-blob = "==12.16.0" -uvicorn = {version = "==0.22.0", extras = ["standard"]} -pydantic = {version = "==1.10.8", extras = ["email"]} -clickhouse-driver = {version = "==0.2.5", extras = ["lz4"]} [dev-packages] diff --git a/ee/api/chalicelib/core/reset_password.py b/ee/api/chalicelib/core/reset_password.py index 889b6d2f8..ef0c14235 100644 --- a/ee/api/chalicelib/core/reset_password.py +++ b/ee/api/chalicelib/core/reset_password.py @@ -2,7 +2,7 @@ from decouple import config import schemas from chalicelib.core import users -from chalicelib.utils import email_helper, captcha, helper +from chalicelib.utils import email_helper, captcha, helper, smtp def reset(data: schemas.ForgetPasswordPayloadSchema): @@ -10,7 +10,7 @@ def reset(data: schemas.ForgetPasswordPayloadSchema): if helper.allow_captcha() and not captcha.is_valid(data.g_recaptcha_response): print("error: Invalid captcha.") return {"errors": ["Invalid captcha."]} - if not helper.has_smtp(): + if not smtp.has_smtp(): return {"errors": ["no SMTP configuration found, you can ask your admin to reset your password"]} a_user = users.get_by_email_only(data.email) if a_user is not None: diff --git a/ee/api/chalicelib/core/users.py b/ee/api/chalicelib/core/users.py index e610bed41..327a11b0e 100644 --- a/ee/api/chalicelib/core/users.py +++ b/ee/api/chalicelib/core/users.py @@ -8,7 +8,7 @@ import schemas import schemas_ee from chalicelib.core import authorizers, metadata, projects, roles from chalicelib.core import tenants, assist -from chalicelib.utils import helper, email_helper +from chalicelib.utils import helper, email_helper, smtp from chalicelib.utils import pg_client from chalicelib.utils.TimeUTC import TimeUTC @@ -575,7 +575,7 @@ def set_password_invitation(tenant_id, user_id, new_password): c = tenants.get_by_tenant_id(tenant_id) c.pop("createdAt") c["projects"] = projects.get_projects(tenant_id=tenant_id, recorded=True, user_id=user_id) - c["smtp"] = helper.has_smtp() + c["smtp"] = smtp.has_smtp() c["iceServers"] = assist.get_ice_servers() return { 'jwt': r.pop('jwt'), diff --git a/ee/api/chalicelib/core/weekly_report.py b/ee/api/chalicelib/core/weekly_report.py index 96bda859e..1684c1419 100644 --- a/ee/api/chalicelib/core/weekly_report.py +++ b/ee/api/chalicelib/core/weekly_report.py @@ -1,4 +1,4 @@ -from chalicelib.utils import pg_client, helper, email_helper +from chalicelib.utils import pg_client, helper, email_helper, smtp from chalicelib.utils.TimeUTC import TimeUTC from chalicelib.utils.helper import get_issue_title @@ -29,7 +29,7 @@ def edit_config(user_id, weekly_report): def cron(): - if not helper.has_smtp(): + if not smtp.has_smtp(): print("!!! No SMTP configuration found, ignoring weekly report") return _now = TimeUTC.now() diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index a42c7d69d..bfd88d01f 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -13,7 +13,7 @@ from chalicelib.core import sessions_viewed from chalicelib.core import tenants, users, projects, license from chalicelib.core import webhook from chalicelib.core.collaboration_slack import Slack -from chalicelib.utils import SAML2_helper +from chalicelib.utils import SAML2_helper, smtp from chalicelib.utils import captcha from chalicelib.utils import helper from chalicelib.utils.TimeUTC import TimeUTC @@ -61,7 +61,7 @@ async def login_user(data: schemas.UserLoginSchema = Body(...)): detail=r["errors"][0] ) - r["smtp"] = helper.has_smtp() + r["smtp"] = smtp.has_smtp() content = { 'jwt': r.pop('jwt'), 'data': { @@ -89,7 +89,7 @@ async def get_account(context: schemas.CurrentContext = Depends(OR_context)): **r, **t, **license.get_status(context.tenant_id), - "smtp": helper.has_smtp(), + "smtp": smtp.has_smtp(), "saml2": SAML2_helper.is_saml2_available(), # "iceServers": assist.get_ice_servers() } diff --git a/ee/api/routers/saml.py b/ee/api/routers/saml.py index eb162db71..cf52aa720 100644 --- a/ee/api/routers/saml.py +++ b/ee/api/routers/saml.py @@ -1,250 +1,249 @@ -# TODO: enable after xmlsec fix from fastapi import HTTPException, Request, Response, status -# from chalicelib.utils import SAML2_helper -# from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth +from chalicelib.utils import SAML2_helper +from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth from routers.base import get_routers 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"]) -# @public_app.get("/sso/saml2/", tags=["saml2"]) -# async def start_sso(request: Request): -# request.path = '' -# req = await prepare_request(request=request) -# auth = init_saml_auth(req) -# sso_built_url = auth.login() -# return RedirectResponse(url=sso_built_url) -# -# -# @public_app.post('/sso/saml2/acs', tags=["saml2"]) -# @public_app.post('/sso/saml2/acs/', tags=["saml2"]) -# async def process_sso_assertion(request: Request): -# req = await prepare_request(request=request) -# session = req["cookie"]["session"] -# auth = init_saml_auth(req) -# -# request_id = None -# if 'AuthNRequestID' in session: -# request_id = session['AuthNRequestID'] -# -# auth.process_response(request_id=request_id) -# errors = auth.get_errors() -# user_data = {} -# if len(errors) == 0: -# if 'AuthNRequestID' in session: -# del session['AuthNRequestID'] -# user_data = auth.get_attributes() -# elif auth.get_settings().is_debug_active(): -# error_reason = auth.get_last_error_reason() -# print("SAML2 error:") -# print(error_reason) -# return {"errors": [error_reason]} -# -# email = auth.get_nameid() -# print("received nameId:") -# print(email) -# existing = users.get_by_email_only(auth.get_nameid()) -# -# internal_id = next(iter(user_data.get("internalId", [])), None) -# tenant_key = user_data.get("tenantKey", []) -# if len(tenant_key) == 0: -# print("tenantKey not present in assertion, please check your SP-assertion-configuration") -# return {"errors": ["tenantKey not present in assertion, please check your SP-assertion-configuration"]} -# else: -# t = tenants.get_by_tenant_key(tenant_key[0]) -# if t is None: -# print("invalid tenantKey, please copy the correct value from Preferences > Account") -# return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]} -# print(user_data) -# role_name = user_data.get("role", []) -# if len(role_name) == 0: -# print("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") -# -# if existing is None: -# deleted = users.get_deleted_user_by_email(auth.get_nameid()) -# if deleted is not None: -# print("== 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"]) -# else: -# print("== 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"]) -# else: -# if t['tenantId'] != existing["tenantId"]: -# print("user exists for a different tenant") -# return {"errors": ["user exists for a different tenant"]} -# if existing.get("origin") is None: -# print(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==") -# users.update(tenant_id=t['tenantId'], user_id=existing["id"], -# changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id}) -# expiration = auth.get_session_expiration() -# expiration = expiration if expiration is not None and expiration > 10 * 60 \ -# else int(config("sso_exp_delta_seconds", cast=int, default=24 * 60 * 60)) -# jwt = users.authenticate_sso(email=email, internal_id=internal_id, exp=expiration) -# if jwt is None: -# return {"errors": ["null JWT"]} -# return Response( -# status_code=status.HTTP_302_FOUND, -# headers={'Location': SAML2_helper.get_landing_URL(jwt)}) -# -# -# @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): -# req = await prepare_request(request=request) -# session = req["cookie"]["session"] -# auth = init_saml_auth(req) -# -# request_id = None -# if 'AuthNRequestID' in session: -# request_id = session['AuthNRequestID'] -# -# auth.process_response(request_id=request_id) -# errors = auth.get_errors() -# user_data = {} -# if len(errors) == 0: -# if 'AuthNRequestID' in session: -# del session['AuthNRequestID'] -# user_data = auth.get_attributes() -# elif auth.get_settings().is_debug_active(): -# error_reason = auth.get_last_error_reason() -# print("SAML2 error:") -# print(error_reason) -# return {"errors": [error_reason]} -# -# email = auth.get_nameid() -# print("received nameId:") -# print(email) -# existing = users.get_by_email_only(auth.get_nameid()) -# -# internal_id = next(iter(user_data.get("internalId", [])), None) -# -# t = tenants.get_by_tenant_key(tenantKey) -# if t is None: -# print("invalid tenantKey, please copy the correct value from Preferences > Account") -# return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]} -# print(user_data) -# role_name = user_data.get("role", []) -# if len(role_name) == 0: -# print("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") -# -# if existing is None: -# deleted = users.get_deleted_user_by_email(auth.get_nameid()) -# if deleted is not None: -# print("== 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"]) -# else: -# print("== 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"]) -# else: -# if t['tenantId'] != existing["tenantId"]: -# print("user exists for a different tenant") -# return {"errors": ["user exists for a different tenant"]} -# if existing.get("origin") is None: -# print(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==") -# users.update(tenant_id=t['tenantId'], user_id=existing["id"], -# changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id}) -# expiration = auth.get_session_expiration() -# expiration = expiration if expiration is not None and expiration > 10 * 60 \ -# else int(config("sso_exp_delta_seconds", cast=int, default=24 * 60 * 60)) -# jwt = users.authenticate_sso(email=email, internal_id=internal_id, exp=expiration) -# if jwt is None: -# return {"errors": ["null JWT"]} -# return Response( -# status_code=status.HTTP_302_FOUND, -# headers={'Location': SAML2_helper.get_landing_URL(jwt)}) -# -# -# @public_app.get('/sso/saml2/sls', tags=["saml2"]) -# @public_app.get('/sso/saml2/sls/', tags=["saml2"]) -# async def process_sls_assertion(request: Request): -# req = await prepare_request(request=request) -# session = req["cookie"]["session"] -# auth = init_saml_auth(req) -# request_id = None -# if 'LogoutRequestID' in session: -# request_id = session['LogoutRequestID'] -# -# def dscb(): -# session.clear() -# -# url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) -# -# errors = auth.get_errors() -# if len(errors) == 0: -# if 'SAMLRequest' in req['get_data']: -# logout_request = OneLogin_Saml2_Logout_Request(auth.get_settings(), req['get_data']['SAMLRequest']) -# user_email = logout_request.get_nameid(auth.get_last_request_xml()) -# to_logout = users.get_by_email_only(user_email) -# -# if len(to_logout) > 0: -# to_logout = to_logout[0]['id'] -# users.change_jwt_iat(to_logout) -# else: -# print("Unknown user SLS-Request By IdP") -# else: -# print("Preprocessed SLS-Request by SP") -# -# if url is not None: -# return RedirectResponse(url=url) -# -# return RedirectResponse(url=config("SITE_URL")) -# -# -# @public_app.get('/sso/saml2/metadata', tags=["saml2"]) -# @public_app.get('/sso/saml2/metadata/', tags=["saml2"]) -# async def saml2_metadata(request: Request): -# req = await prepare_request(request=request) -# auth = init_saml_auth(req) -# settings = auth.get_settings() -# metadata = settings.get_sp_metadata() -# errors = settings.validate_metadata(metadata) -# -# if len(errors) == 0: -# return Response( -# status_code=status.HTTP_200_OK, -# content=metadata, -# headers={'Content-Type': 'text/xml'}) -# else: -# raise HTTPException( -# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, -# detail=', '.join(errors)) +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"]) +@public_app.get("/sso/saml2/", tags=["saml2"]) +async def start_sso(request: Request): + request.path = '' + req = await prepare_request(request=request) + auth = init_saml_auth(req) + sso_built_url = auth.login() + return RedirectResponse(url=sso_built_url) + + +@public_app.post('/sso/saml2/acs', tags=["saml2"]) +@public_app.post('/sso/saml2/acs/', tags=["saml2"]) +async def process_sso_assertion(request: Request): + req = await prepare_request(request=request) + session = req["cookie"]["session"] + auth = init_saml_auth(req) + + request_id = None + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + + auth.process_response(request_id=request_id) + errors = auth.get_errors() + user_data = {} + if len(errors) == 0: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] + user_data = auth.get_attributes() + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + print("SAML2 error:") + print(error_reason) + return {"errors": [error_reason]} + + email = auth.get_nameid() + print("received nameId:") + print(email) + existing = users.get_by_email_only(auth.get_nameid()) + + internal_id = next(iter(user_data.get("internalId", [])), None) + tenant_key = user_data.get("tenantKey", []) + if len(tenant_key) == 0: + print("tenantKey not present in assertion, please check your SP-assertion-configuration") + return {"errors": ["tenantKey not present in assertion, please check your SP-assertion-configuration"]} + else: + t = tenants.get_by_tenant_key(tenant_key[0]) + if t is None: + print("invalid tenantKey, please copy the correct value from Preferences > Account") + return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]} + print(user_data) + role_name = user_data.get("role", []) + if len(role_name) == 0: + print("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") + + if existing is None: + deleted = users.get_deleted_user_by_email(auth.get_nameid()) + if deleted is not None: + print("== 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"]) + else: + print("== 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"]) + else: + if t['tenantId'] != existing["tenantId"]: + print("user exists for a different tenant") + return {"errors": ["user exists for a different tenant"]} + if existing.get("origin") is None: + print(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==") + users.update(tenant_id=t['tenantId'], user_id=existing["id"], + changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id}) + expiration = auth.get_session_expiration() + expiration = expiration if expiration is not None and expiration > 10 * 60 \ + else int(config("sso_exp_delta_seconds", cast=int, default=24 * 60 * 60)) + jwt = users.authenticate_sso(email=email, internal_id=internal_id, exp=expiration) + if jwt is None: + return {"errors": ["null JWT"]} + return Response( + status_code=status.HTTP_302_FOUND, + headers={'Location': SAML2_helper.get_landing_URL(jwt)}) + + +@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): + req = await prepare_request(request=request) + session = req["cookie"]["session"] + auth = init_saml_auth(req) + + request_id = None + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + + auth.process_response(request_id=request_id) + errors = auth.get_errors() + user_data = {} + if len(errors) == 0: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] + user_data = auth.get_attributes() + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + print("SAML2 error:") + print(error_reason) + return {"errors": [error_reason]} + + email = auth.get_nameid() + print("received nameId:") + print(email) + existing = users.get_by_email_only(auth.get_nameid()) + + internal_id = next(iter(user_data.get("internalId", [])), None) + + t = tenants.get_by_tenant_key(tenantKey) + if t is None: + print("invalid tenantKey, please copy the correct value from Preferences > Account") + return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]} + print(user_data) + role_name = user_data.get("role", []) + if len(role_name) == 0: + print("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") + + if existing is None: + deleted = users.get_deleted_user_by_email(auth.get_nameid()) + if deleted is not None: + print("== 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"]) + else: + print("== 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"]) + else: + if t['tenantId'] != existing["tenantId"]: + print("user exists for a different tenant") + return {"errors": ["user exists for a different tenant"]} + if existing.get("origin") is None: + print(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==") + users.update(tenant_id=t['tenantId'], user_id=existing["id"], + changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id}) + expiration = auth.get_session_expiration() + expiration = expiration if expiration is not None and expiration > 10 * 60 \ + else int(config("sso_exp_delta_seconds", cast=int, default=24 * 60 * 60)) + jwt = users.authenticate_sso(email=email, internal_id=internal_id, exp=expiration) + if jwt is None: + return {"errors": ["null JWT"]} + return Response( + status_code=status.HTTP_302_FOUND, + headers={'Location': SAML2_helper.get_landing_URL(jwt)}) + + +@public_app.get('/sso/saml2/sls', tags=["saml2"]) +@public_app.get('/sso/saml2/sls/', tags=["saml2"]) +async def process_sls_assertion(request: Request): + req = await prepare_request(request=request) + session = req["cookie"]["session"] + auth = init_saml_auth(req) + request_id = None + if 'LogoutRequestID' in session: + request_id = session['LogoutRequestID'] + + def dscb(): + session.clear() + + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) + + errors = auth.get_errors() + if len(errors) == 0: + if 'SAMLRequest' in req['get_data']: + logout_request = OneLogin_Saml2_Logout_Request(auth.get_settings(), req['get_data']['SAMLRequest']) + user_email = logout_request.get_nameid(auth.get_last_request_xml()) + to_logout = users.get_by_email_only(user_email) + + if len(to_logout) > 0: + to_logout = to_logout[0]['id'] + users.change_jwt_iat(to_logout) + else: + print("Unknown user SLS-Request By IdP") + else: + print("Preprocessed SLS-Request by SP") + + if url is not None: + return RedirectResponse(url=url) + + return RedirectResponse(url=config("SITE_URL")) + + +@public_app.get('/sso/saml2/metadata', tags=["saml2"]) +@public_app.get('/sso/saml2/metadata/', tags=["saml2"]) +async def saml2_metadata(request: Request): + req = await prepare_request(request=request) + auth = init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + return Response( + status_code=status.HTTP_200_OK, + content=metadata, + headers={'Content-Type': 'text/xml'}) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=', '.join(errors))