From 36e0c7c421a11f7d14866e966860c55a5c39000f Mon Sep 17 00:00:00 2001 From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com> Date: Sun, 2 Jun 2024 17:41:16 +0200 Subject: [PATCH 001/115] WIP --- backend/test_saml.py | 147 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 backend/test_saml.py diff --git a/backend/test_saml.py b/backend/test_saml.py new file mode 100644 index 000000000..b0c74b7c3 --- /dev/null +++ b/backend/test_saml.py @@ -0,0 +1,147 @@ +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.utils import OneLogin_Saml2_Utils +import json + +SETTINGS_DATA = """{ + // If strict is True, then the Python Toolkit will reject unsigned + // or unencrypted messages if it expects them to be signed or encrypted. + // Also it will reject the messages if the SAML standard is not strictly + // followed. Destination, NameId, Conditions ... are validated too. + "strict": true, + // Enable debug mode (outputs errors). + "debug": true, + // Service Provider Data that we are deploying. + "sp": { + // Identifier of the SP entity (must be a URI) + "entityId": "https://localhost:8443/metadata/", + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + "assertionConsumerService": { + // URL Location where the from the IdP will be returned + "url": "https://localhost:8443/?acs", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports this endpoint for the + // HTTP-POST binding only. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, + // Specifies info about where and how the message MUST be sent. + "singleLogoutService": { + // URL Location where the from the IdP will be sent (IdP-initiated logout) + "url": "https://localhost:8443/?sls", + // URL Location where the from the IdP will sent (SP-initiated logout, reply) + // OPTIONAL: only specify if different from url parameter + //"responseUrl": "https://localhost:8443/?sls", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + // If you need to specify requested attributes, set a + // attributeConsumingService. nameFormat, attributeValue and + // friendlyName can be omitted + //"attributeConsumingService": { + // OPTIONAL: only specify if SP requires this. + // index is an integer which identifies the attributeConsumingService used + // to the SP. SAML toolkit supports configuring only one attributeConsumingService + // but in certain cases the SP requires a different value. Defaults to '1'. + // "index": '1', + // "serviceName": "SP test", + // "serviceDescription": "Test Service", + // "requestedAttributes": [ + // { + // "name": "", + // "isRequired": false, + // "nameFormat": "", + // "friendlyName": "", + // "attributeValue": [] + // } + // ] + //}, + // Specifies the constraints on the name identifier to be used to + // represent the requested subject. + // Take a look on src/onelogin/saml2/constants.py to see the NameIdFormat that are supported. + "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + // Usually X.509 cert and privateKey of the SP are provided by files placed at + // the certs folder. But we can also provide them with the following parameters + //"x509cert": "", + //"privateKey": "" + // + // Key rollover + // If you plan to update the SP X.509cert and privateKey + // you can define here the new X.509cert and it will be + // published on the SP metadata so Identity Providers can + // read them and get ready for rollover. + // + // 'x509certNew': '', + }, + // Identity Provider Data that we want connected with our SP. + "idp": { + // Identifier of the IdP entity (must be a URI) + "entityId": "https://app.onelogin.com/saml/metadata/", + // SSO endpoint info of the IdP. (Authentication Request protocol) + "singleSignOnService": { + // URL Target of the IdP where the Authentication Request Message + // will be sent. + "url": "https://app.onelogin.com/trust/saml2/http-post/sso/", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + // SLO endpoint info of the IdP. + "singleLogoutService": { + // URL Location where the from the IdP will be sent (IdP-initiated logout) + "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/", + // URL Location where the from the IdP will sent (SP-initiated logout, reply) + // OPTIONAL: only specify if different from url parameter + "responseUrl": "https://app.onelogin.com/trust/saml2/http-redirect/slo_return/", + // SAML protocol binding to be used when returning the + // message. SAML Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + }, + // Public X.509 certificate of the IdP + "x509cert": "" + // + // Instead of using the whole X.509cert you can use a fingerprint in order to + // validate a SAMLResponse (but you still need the X.509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding). + // But take in mind that the algorithm for the fingerprint should be as strong as the algorithm in a normal certificate signature + // e.g. SHA256 or strong) + // + // (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, + // or add for example the -sha256 , -sha384 or -sha512 parameter) + // + // If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to + // let the toolkit know which algorithm was used. + //ossible values: sha1, sha256, sha384 or sha512 + // 'sha1' is the default value. + // + // Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you + // will need to provide the whole X.509cert. + // + // "certFingerprint": "", + // "certFingerprintAlgorithm": "sha1", + // In some scenarios the IdP uses different certificates for + // signing/encryption, or is under key rollover phase and + // more than one certificate is published on IdP metadata. + // In order to handle that the toolkit offers that parameter. + // (when used, 'X.509cert' and 'certFingerprint' values are + // ignored). + // + // 'x509certMulti': { + // 'signing': [ + // '' + // ], + // 'encryption': [ + // '' + // ] + // } + } +} +""" + +s = ''.join(l+'\n' if not l.lstrip().startswith('//') else '' for l in SETTINGS_DATA.split('\n')) +for i, s2 in enumerate(s.split('\n'), start=1): + print(i, s2) +settings = OneLogin_Saml2_Settings(json.loads(s)) \ No newline at end of file From 0567ebe0ff871c32af75cd5bf718d54a83797103 Mon Sep 17 00:00:00 2001 From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:39:21 +0200 Subject: [PATCH 002/115] WIP --- backend/test_saml.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/test_saml.py b/backend/test_saml.py index b0c74b7c3..6cfef1673 100644 --- a/backend/test_saml.py +++ b/backend/test_saml.py @@ -102,7 +102,7 @@ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, // Public X.509 certificate of the IdP - "x509cert": "" + "x509cert": "MIICmzCCAYMCBgGP2auE8DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNjAyMTU1NTQ3WhcNMzQwNjAyMTU1NzI3WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN41sTNMtfd8FG9BENArR6czvf7CnkSeD" // // Instead of using the whole X.509cert you can use a fingerprint in order to // validate a SAMLResponse (but you still need the X.509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding). @@ -142,6 +142,6 @@ """ s = ''.join(l+'\n' if not l.lstrip().startswith('//') else '' for l in SETTINGS_DATA.split('\n')) -for i, s2 in enumerate(s.split('\n'), start=1): - print(i, s2) -settings = OneLogin_Saml2_Settings(json.loads(s)) \ No newline at end of file +#for i, s2 in enumerate(s.split('\n'), start=1): +# print(i, s2) +settings = OneLogin_Saml2_Settings(json.loads(s)) From 6859646baa1de7e93889fe0ee01e3cda6c7ef073 Mon Sep 17 00:00:00 2001 From: Mohamed-Hacene Date: Tue, 11 Jun 2024 13:47:01 +0200 Subject: [PATCH 003/115] feat: saml --- backend/ciso_assistant/settings.py | 84 ++++++++++++++- backend/core/urls.py | 2 + backend/iam/adapter.py | 27 +++++ backend/iam/views.py | 1 + backend/requirements.txt | 1 + frontend/src/lib/allauth.js | 100 ++++++++++++++++++ frontend/src/lib/django.js | 18 ++++ .../(authentication)/login/+page.svelte | 30 +++++- 8 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 backend/iam/adapter.py create mode 100644 frontend/src/lib/allauth.js create mode 100644 frontend/src/lib/django.js diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py index 10aed45b1..b922ad84b 100644 --- a/backend/ciso_assistant/settings.py +++ b/backend/ciso_assistant/settings.py @@ -99,7 +99,8 @@ def set_ciso_assistant_url(_, __, event_dict): logger.info("DEBUG mode: %s", DEBUG) logger.info("CISO_ASSISTANT_URL: %s", CISO_ASSISTANT_URL) # ALLOWED_HOSTS should contain the backend address -ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") +# ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") +ALLOWED_HOSTS = ['*'] logger.info("ALLOWED_HOSTS: %s", ALLOWED_HOSTS) CSRF_TRUSTED_ORIGINS = [CISO_ASSISTANT_URL] LOCAL_STORAGE_DIRECTORY = os.environ.get( @@ -130,6 +131,11 @@ def set_ciso_assistant_url(_, __, event_dict): "rest_framework", "knox", "drf_spectacular", + 'allauth', + 'allauth.account', + 'allauth.headless', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.saml', ] MIDDLEWARE = [ @@ -137,16 +143,17 @@ def set_ciso_assistant_url(_, __, event_dict): "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", + # "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django_structlog.middlewares.RequestMiddleware", + "allauth.account.middleware.AccountMiddleware", ] ROOT_URLCONF = "ciso_assistant.urls" -LOGIN_REDIRECT_URL = "home" -LOGOUT_REDIRECT_URL = "login" +LOGIN_REDIRECT_URL = "/api" +LOGOUT_REDIRECT_URL = "/api" AUTH_TOKEN_TTL = int( os.environ.get("AUTH_TOKEN_TTL", default=60 * 15) @@ -322,3 +329,72 @@ def set_ciso_assistant_url(_, __, event_dict): "SERVE_INCLUDE_SCHEMA": False, # OTHER SETTINGS } + +#SSO with allauth + +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_AUTHENTICATION_METHOD = 'email' + +ACCOUNT_ADAPTER = 'iam.adapter.MyAccountAdapter' +SOCIALACCOUNT_ADAPTER = 'iam.adapter.MySocialAccountAdapter' + +SOCIALACCOUNT_EMAIL_AUTHENTICATION = True +SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True + +HEADLESS_ONLY = True + +# HEADLESS_FRONTEND_URLS = { +# "socialaccount_login_error": "http://localhost:5173/", +# } + +SOCIALACCOUNT_PROVIDERS = { + "saml": { + # Here, each app represents the SAML provider configuration of one + # organization. + 'EMAIL_AUTHENTICATION': True, + "VERIFIED_EMAIL": True, + "APPS": [ + { + "name": "Keycloack", + "provider_id": "http://127.0.0.1:8000/api/accounts/saml/keycloack/metadata", + "client_id": "keycloack", + "settings": { + "attribute_mapping": { + "uid": "", + "email_verified": "", + "email": "emailAdress", + }, + "idp": { + "entity_id": "http://localhost:8080/realms/cisodev", + "metadata_url": "http://localhost:8080/realms/cisodev/protocol/saml/descriptor", + }, + "sp": { + "entity_id": "http://127.0.0.1:8000/api/accounts/saml/keycloack/metadata", + }, + "advanced": { + "allow_repeat_attribute_name": True, + "allow_single_label_domains": False, + "authn_request_signed": False, + "digest_algorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "logout_request_signed": False, + "logout_response_signed": False, + "metadata_signed": False, + "name_id_encrypted": False, + "reject_deprecated_algorithm": True, + # Due to security concerns, IdP initiated SSO is rejected by default. + "reject_idp_initiated_sso": True, + "signature_algorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "want_assertion_encrypted": False, + "want_assertion_signed": False, + "want_attribute_statement": True, + "want_message_signed": False, + "want_name_id": False, + "want_name_id_encrypted": False, + }, + }, + }, + ], + } +} diff --git a/backend/core/urls.py b/backend/core/urls.py index 2f8062ee6..63ec4aa58 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -55,6 +55,8 @@ path("agg_data/", get_agg_data, name="get_agg_data"), path("composer_data/", get_composer_data, name="get_composer_data"), path("i18n/", include("django.conf.urls.i18n")), + path('accounts/', include('allauth.urls')), + path("_allauth/", include("allauth.headless.urls")), ] if DEBUG: diff --git a/backend/iam/adapter.py b/backend/iam/adapter.py new file mode 100644 index 000000000..7e75e4727 --- /dev/null +++ b/backend/iam/adapter.py @@ -0,0 +1,27 @@ +from django.conf import settings +from allauth.account.adapter import DefaultAccountAdapter +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.dispatch import receiver +from allauth.socialaccount.signals import pre_social_login +from django.contrib.auth import login, get_user_model +from rest_framework.response import Response +from rest_framework.status import HTTP_401_UNAUTHORIZED +from knox.views import LoginView + +User = get_user_model() + +class MyAccountAdapter(DefaultAccountAdapter): + + def is_open_for_signup(self, request): + return False + + +class MySocialAccountAdapter(DefaultSocialAccountAdapter): + + def pre_social_login(self, request, sociallogin): + email_address = next(iter(sociallogin.account.extra_data.values()))[0] + try: + user = User.objects.get(email=email_address) + sociallogin.user = user + except User.DoesNotExist: + return Response({"message": "User not found."}, status=HTTP_401_UNAUTHORIZED) diff --git a/backend/iam/views.py b/backend/iam/views.py index 464a21b8a..5f58d1576 100644 --- a/backend/iam/views.py +++ b/backend/iam/views.py @@ -51,6 +51,7 @@ class LogoutView(views.APIView): def post(self, request) -> Response: try: logger.info("logout request", user=request.user) + print("logout request", request.user) logout(request) logger.info("logout successful", user=request.user) except Exception as e: diff --git a/backend/requirements.txt b/backend/requirements.txt index f019233cc..b7cca8bb7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,3 +19,4 @@ python-dotenv==1.0.1 drf-spectacular==0.27.2 django-rest-knox==4.2.0 pre-commit==3.7.0 +django-allauth[socialaccount]>=0.63.3 \ No newline at end of file diff --git a/frontend/src/lib/allauth.js b/frontend/src/lib/allauth.js new file mode 100644 index 000000000..6ecc0bab7 --- /dev/null +++ b/frontend/src/lib/allauth.js @@ -0,0 +1,100 @@ +import { getCSRFToken } from './django.js' +import { BASE_API_URL } from '$lib/utils/constants'; + +const Client = Object.freeze({ + APP: 'app', + BROWSER: 'browser' +}) + +const CLIENT = Client.BROWSER + +const BASE_URL = `${BASE_API_URL}/_allauth/${CLIENT}/v1` +const ACCEPT_JSON = { + accept: 'application/json' +} + +export const AuthProcess = Object.freeze({ + LOGIN: 'login', + CONNECT: 'connect' +}) + +export const Flows = Object.freeze({ + VERIFY_EMAIL: 'verify_email', + LOGIN: 'login', + LOGIN_BY_CODE: 'login_by_code', + SIGNUP: 'signup', + PROVIDER_REDIRECT: 'provider_redirect', + PROVIDER_SIGNUP: 'provider_signup', + MFA_AUTHENTICATE: 'mfa_authenticate', + REAUTHENTICATE: 'reauthenticate', + MFA_REAUTHENTICATE: 'mfa_reauthenticate' +}) + +export const URLs = Object.freeze({ + // Meta + CONFIG: BASE_URL + '/config', + + // Account management + CHANGE_PASSWORD: BASE_URL + '/account/password/change', + EMAIL: BASE_URL + '/account/email', + PROVIDERS: BASE_URL + '/account/providers', + + // Account management: 2FA + AUTHENTICATORS: BASE_URL + '/account/authenticators', + RECOVERY_CODES: BASE_URL + '/account/authenticators/recovery-codes', + TOTP_AUTHENTICATOR: BASE_URL + '/account/authenticators/totp', + + // Auth: Basics + LOGIN: BASE_URL + '/auth/login', + REQUEST_LOGIN_CODE: BASE_URL + '/auth/code/request', + CONFIRM_LOGIN_CODE: BASE_URL + '/auth/code/confirm', + SESSION: BASE_URL + '/auth/session', + REAUTHENTICATE: BASE_URL + '/auth/reauthenticate', + REQUEST_PASSWORD_RESET: BASE_URL + '/auth/password/request', + RESET_PASSWORD: BASE_URL + '/auth/password/reset', + SIGNUP: BASE_URL + '/auth/signup', + VERIFY_EMAIL: BASE_URL + '/auth/email/verify', + + // Auth: 2FA + MFA_AUTHENTICATE: BASE_URL + '/auth/2fa/authenticate', + MFA_REAUTHENTICATE: BASE_URL + '/auth/2fa/reauthenticate', + + // Auth: Social + PROVIDER_SIGNUP: BASE_URL + '/auth/provider/signup', + REDIRECT_TO_PROVIDER: BASE_URL + '/auth/provider/redirect', + PROVIDER_TOKEN: BASE_URL + '/auth/provider/token', + + // Auth: Sessions + SESSIONS: BASE_URL + '/auth/sessions' +}) + +export const AuthenticatorType = Object.freeze({ + TOTP: 'totp', + RECOVERY_CODES: 'recovery_codes' +}) + +function postForm (action, data) { + const f = document.createElement('form') + f.method = 'POST' + f.action = action + + for (const key in data) { + const d = document.createElement('input') + d.type = 'hidden' + d.name = key + d.value = data[key] + f.appendChild(d) + } + document.body.appendChild(f) + f.submit() +} + + +export function redirectToProvider (providerId, callbackURL, process = AuthProcess.LOGIN) { + postForm(URLs.REDIRECT_TO_PROVIDER, { + provider: providerId, + process, + callback_url: callbackURL, + csrfmiddlewaretoken: getCSRFToken() + }) +} diff --git a/frontend/src/lib/django.js b/frontend/src/lib/django.js new file mode 100644 index 000000000..ba9e6117b --- /dev/null +++ b/frontend/src/lib/django.js @@ -0,0 +1,18 @@ +function getCookie (name) { + let cookieValue = null + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';') + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim() + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)) + break + } + } + } + return cookieValue +} +export function getCSRFToken () { + return getCookie('csrftoken') +} diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte index 6ee1043e7..2e2e95158 100644 --- a/frontend/src/routes/(authentication)/login/+page.svelte +++ b/frontend/src/routes/(authentication)/login/+page.svelte @@ -5,13 +5,40 @@ import TextField from '$lib/components/Forms/TextField.svelte'; import SuperForm from '$lib/components/Forms/Form.svelte'; import Typewriter from 'svelte-typewriter'; + import { onMount } from 'svelte'; import * as m from '$paraglide/messages.js'; import { zod } from 'sveltekit-superforms/adapters'; + import { redirectToProvider } from '$lib/allauth'; export let data: PageData; - const cursor = false; + + + // onMount(() => { + // var myHeaders = new Headers(); + // myHeaders.append("Content-Type", "application/x-www-form-urlencoded"); + // myHeaders.append("Cookie", "csrftoken=6r97z10ETbmk9YbzMs94THfigy7G5joO; sessionid=a9ntqwovzdqubh2xtedjlxfhf5sy7q3u"); + + // var urlencoded = new URLSearchParams(); + // urlencoded.append("provider", "http://127.0.0.1:8000/api/accounts/saml/keycloack/metadata"); + // urlencoded.append("callback_url", "http://127.0.0.1:8000/api"); + // urlencoded.append("process", "login"); + + // var requestOptions = { + // mode: 'no-cors', + // method: 'POST', + // headers: myHeaders, + // body: urlencoded, + // redirect: 'follow' + // }; + + // const res = fetch("http://127.0.0.1:8000/api/_allauth/browser/v1/auth/provider/redirect", requestOptions) + // .then(response => console.log(response)) + // .then(result => console.log(result)) + // .catch(error => console.log('error', error)); + // console.log(res); + // });
@@ -78,6 +105,7 @@

+ From 7e77e53b11a87fdd15580e03f3c75bfd4c406c4b Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Tue, 11 Jun 2024 18:28:27 +0200 Subject: [PATCH 004/115] SAML PoC --- backend/ciso_assistant/settings.py | 28 ++--- backend/core/urls.py | 4 +- backend/iam/adapter.py | 14 ++- backend/iam/sso/__init__.py | 0 backend/iam/sso/saml/__init__.py | 0 backend/iam/sso/saml/urls.py | 24 ++++ backend/iam/sso/saml/views.py | 116 ++++++++++++++++++ backend/iam/utils.py | 6 + .../(authentication)/login/+page.svelte | 11 +- .../sso/authenticate/[token]/+page.server.ts | 17 +++ 10 files changed, 197 insertions(+), 23 deletions(-) create mode 100644 backend/iam/sso/__init__.py create mode 100644 backend/iam/sso/saml/__init__.py create mode 100644 backend/iam/sso/saml/urls.py create mode 100644 backend/iam/sso/saml/views.py create mode 100644 backend/iam/utils.py create mode 100644 frontend/src/routes/(authentication)/sso/authenticate/[token]/+page.server.ts diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py index b922ad84b..db79020e9 100644 --- a/backend/ciso_assistant/settings.py +++ b/backend/ciso_assistant/settings.py @@ -100,7 +100,7 @@ def set_ciso_assistant_url(_, __, event_dict): logger.info("CISO_ASSISTANT_URL: %s", CISO_ASSISTANT_URL) # ALLOWED_HOSTS should contain the backend address # ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] logger.info("ALLOWED_HOSTS: %s", ALLOWED_HOSTS) CSRF_TRUSTED_ORIGINS = [CISO_ASSISTANT_URL] LOCAL_STORAGE_DIRECTORY = os.environ.get( @@ -131,11 +131,11 @@ def set_ciso_assistant_url(_, __, event_dict): "rest_framework", "knox", "drf_spectacular", - 'allauth', - 'allauth.account', - 'allauth.headless', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.saml', + "allauth", + "allauth.account", + "allauth.headless", + "allauth.socialaccount", + "allauth.socialaccount.providers.saml", ] MIDDLEWARE = [ @@ -330,30 +330,30 @@ def set_ciso_assistant_url(_, __, event_dict): # OTHER SETTINGS } -#SSO with allauth +# SSO with allauth ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False -ACCOUNT_AUTHENTICATION_METHOD = 'email' +ACCOUNT_AUTHENTICATION_METHOD = "email" -ACCOUNT_ADAPTER = 'iam.adapter.MyAccountAdapter' -SOCIALACCOUNT_ADAPTER = 'iam.adapter.MySocialAccountAdapter' +ACCOUNT_ADAPTER = "iam.adapter.MyAccountAdapter" +SOCIALACCOUNT_ADAPTER = "iam.adapter.MySocialAccountAdapter" SOCIALACCOUNT_EMAIL_AUTHENTICATION = True SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True HEADLESS_ONLY = True -# HEADLESS_FRONTEND_URLS = { -# "socialaccount_login_error": "http://localhost:5173/", -# } +HEADLESS_FRONTEND_URLS = { + "socialaccount_login_error": "http://localhost:5173/", +} SOCIALACCOUNT_PROVIDERS = { "saml": { # Here, each app represents the SAML provider configuration of one # organization. - 'EMAIL_AUTHENTICATION': True, + "EMAIL_AUTHENTICATION": True, "VERIFIED_EMAIL": True, "APPS": [ { diff --git a/backend/core/urls.py b/backend/core/urls.py index 63ec4aa58..da0ad92ec 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -1,5 +1,6 @@ from .views import * from library.views import StoredLibraryViewSet, LoadedLibraryViewSet +from iam.sso.saml.views import FinishACSView from django.urls import include, path @@ -55,8 +56,9 @@ path("agg_data/", get_agg_data, name="get_agg_data"), path("composer_data/", get_composer_data, name="get_composer_data"), path("i18n/", include("django.conf.urls.i18n")), - path('accounts/', include('allauth.urls')), + path("accounts/", include("allauth.urls")), path("_allauth/", include("allauth.headless.urls")), + path("accounts/saml/", include("iam.sso.saml.urls")), ] if DEBUG: diff --git a/backend/iam/adapter.py b/backend/iam/adapter.py index 7e75e4727..c6dc2f762 100644 --- a/backend/iam/adapter.py +++ b/backend/iam/adapter.py @@ -1,3 +1,5 @@ +from allauth.account.utils import perform_login +from allauth.socialaccount.helpers import ImmediateHttpResponse from django.conf import settings from allauth.account.adapter import DefaultAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter @@ -10,18 +12,20 @@ User = get_user_model() -class MyAccountAdapter(DefaultAccountAdapter): +class MyAccountAdapter(DefaultAccountAdapter): def is_open_for_signup(self, request): return False - - -class MySocialAccountAdapter(DefaultSocialAccountAdapter): + +class MySocialAccountAdapter(DefaultSocialAccountAdapter): def pre_social_login(self, request, sociallogin): email_address = next(iter(sociallogin.account.extra_data.values()))[0] try: user = User.objects.get(email=email_address) sociallogin.user = user + sociallogin.connect(request, user) except User.DoesNotExist: - return Response({"message": "User not found."}, status=HTTP_401_UNAUTHORIZED) + return Response( + {"message": "User not found."}, status=HTTP_401_UNAUTHORIZED + ) diff --git a/backend/iam/sso/__init__.py b/backend/iam/sso/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/iam/sso/saml/__init__.py b/backend/iam/sso/saml/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/iam/sso/saml/urls.py b/backend/iam/sso/saml/urls.py new file mode 100644 index 000000000..2763a6dae --- /dev/null +++ b/backend/iam/sso/saml/urls.py @@ -0,0 +1,24 @@ +from django.urls import include, path, re_path + +from . import views + + +urlpatterns = [ + re_path( + r"^saml/(?P[^/]+)/", + include( + [ + path( + "acs/", + views.ACSView.as_view(), + name="saml_acs", + ), + path( + "acs/finish/", + views.FinishACSView.as_view(), + name="saml_finish_acs", + ), + ] + ), + ) +] diff --git a/backend/iam/sso/saml/views.py b/backend/iam/sso/saml/views.py new file mode 100644 index 000000000..bc20317a7 --- /dev/null +++ b/backend/iam/sso/saml/views.py @@ -0,0 +1,116 @@ +from allauth.account.utils import Login +from allauth.socialaccount.models import SocialLogin +from allauth.socialaccount.providers.saml.views import ( + AuthError, + AuthProcess, + LoginSession, + OneLogin_Saml2_Error, + SAMLViewMixin, + binascii, + build_auth, + complete_social_login, + decode_relay_state, + httpkit, + render_authentication_error, +) +from django.http import HttpRequest, HttpResponseRedirect +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View +from rest_framework.views import csrf_exempt + +from pprint import pprint + +import structlog + +from iam.models import User +from iam.utils import generate_token + +logger = structlog.get_logger(__name__) + + +@method_decorator(csrf_exempt, name="dispatch") +class ACSView(SAMLViewMixin, View): + def dispatch(self, request, organization_slug): + url = reverse( + "saml_finish_acs", + kwargs={"organization_slug": organization_slug}, + ) + response = HttpResponseRedirect(url) + acs_session = LoginSession(request, "saml_acs_session", "saml-acs-session") + acs_session.store.update({"request": httpkit.serialize_request(request)}) + acs_session.save(response) + return response + + +class FinishACSView(SAMLViewMixin, View): + def dispatch(self, request, organization_slug): + provider = self.get_provider(organization_slug) + acs_session = LoginSession(request, "saml_acs_session", "saml-acs-session") + acs_request = None + acs_request_data = acs_session.store.get("request") + if acs_request_data: + acs_request = httpkit.deserialize_request(acs_request_data, HttpRequest()) + acs_session.delete() + if not acs_request: + logger.error("Unable to finish login, SAML ACS session missing") + return render_authentication_error(request, provider) + + auth = build_auth(acs_request, provider) + error_reason = None + errors = [] + try: + # We're doing the check for a valid `InResponeTo` ourselves later on + # (*) by checking if there is a matching state stashed. + auth.process_response(request_id=None) + except binascii.Error: + errors = ["invalid_response"] + error_reason = "Invalid response" + except OneLogin_Saml2_Error as e: + errors = ["error"] + error_reason = str(e) + if not errors: + errors = auth.get_errors() + if errors: + # e.g. ['invalid_response'] + error_reason = auth.get_last_error_reason() or error_reason + logger.error( + "Error processing SAML ACS response: %s: %s" + % (", ".join(errors), error_reason) + ) + return render_authentication_error( + request, + provider, + extra_context={ + "saml_errors": errors, + "saml_last_error_reason": error_reason, + }, + ) + if not auth.is_authenticated(): + return render_authentication_error( + request, provider, error=AuthError.CANCELLED + ) + login: SocialLogin = provider.sociallogin_from_response(request, auth) + # (*) If we (the SP) initiated the login, there should be a matching + # state. + state_id = auth.get_last_response_in_response_to() + if state_id: + login.state = provider.unstash_redirect_state(request, state_id) + else: + # IdP initiated SSO + reject = provider.app.settings.get("advanced", {}).get( + "reject_idp_initiated_sso", True + ) + if reject: + logger.error("IdP initiated SSO rejected") + return render_authentication_error(request, provider) + next_url = decode_relay_state(acs_request.POST.get("RelayState")) + login.state["process"] = AuthProcess.LOGIN + if next_url: + login.state["next"] = next_url + print("LOGIN STATE", login.state) + email = auth._friendlyname_attributes.get("email")[0] + user = User.objects.get(email=email) + token = generate_token(user) + login.state["next"] += f"sso/authenticate/{token}" + return complete_social_login(request, login) diff --git a/backend/iam/utils.py b/backend/iam/utils.py new file mode 100644 index 000000000..1cda362ff --- /dev/null +++ b/backend/iam/utils.py @@ -0,0 +1,6 @@ +from knox.auth import AuthToken + + +def generate_token(user): + _auth_token = AuthToken.objects.create(user=user) + return _auth_token[1] diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte index 2e2e95158..5a5bde0f0 100644 --- a/frontend/src/routes/(authentication)/login/+page.svelte +++ b/frontend/src/routes/(authentication)/login/+page.svelte @@ -13,8 +13,6 @@ export let data: PageData; - - // onMount(() => { // var myHeaders = new Headers(); // myHeaders.append("Content-Type", "application/x-www-form-urlencoded"); @@ -105,7 +103,14 @@

- + diff --git a/frontend/src/routes/(authentication)/sso/authenticate/[token]/+page.server.ts b/frontend/src/routes/(authentication)/sso/authenticate/[token]/+page.server.ts new file mode 100644 index 000000000..460e2aada --- /dev/null +++ b/frontend/src/routes/(authentication)/sso/authenticate/[token]/+page.server.ts @@ -0,0 +1,17 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ locals, params, cookies }) => { + if (locals.user) { + redirect(302, '/analytics'); + } + + cookies.set('token', params.token, { + httpOnly: true, + sameSite: 'lax', + path: '/', + secure: true + }); + + redirect(302, '/analytics'); +}; From 1633a3dd2efee8a22a5c255d498f2ed26cc41ef6 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Tue, 11 Jun 2024 19:11:30 +0200 Subject: [PATCH 005/115] Add CRUD endpoints for identity providers --- backend/core/apps.py | 8 +++++++ backend/core/views.py | 4 +++- .../iam/migrations/0004_identityprovider.py | 23 +++++++++++++++++++ backend/iam/sso/models.py | 6 +++++ backend/iam/sso/serializers.py | 15 ++++++++++++ backend/iam/sso/urls.py | 15 ++++++++++++ backend/iam/sso/views.py | 10 ++++++++ backend/iam/urls.py | 3 ++- 8 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 backend/iam/migrations/0004_identityprovider.py create mode 100644 backend/iam/sso/models.py create mode 100644 backend/iam/sso/serializers.py create mode 100644 backend/iam/sso/urls.py create mode 100644 backend/iam/sso/views.py diff --git a/backend/core/apps.py b/backend/core/apps.py index b90f7359c..df1063591 100644 --- a/backend/core/apps.py +++ b/backend/core/apps.py @@ -26,6 +26,7 @@ "view_loadedlibrary", "view_storedlibrary", "view_user", + "view_identityprovider", ] APPROVER_PERMISSIONS_LIST = [ @@ -50,6 +51,7 @@ "view_storedlibrary", "view_loadedlibrary", "view_user", + "view_identityprovider", ] ANALYST_PERMISSIONS_LIST = [ @@ -104,6 +106,7 @@ "view_storedlibrary", "view_loadedlibrary", "view_user", + "view_identityprovider", ] DOMAIN_MANAGER_PERMISSIONS_LIST = [ @@ -163,6 +166,7 @@ "view_storedlibrary", "view_loadedlibrary", "view_user", + "view_identityprovider", ] ADMINISTRATOR_PERMISSIONS_LIST = [ @@ -245,6 +249,10 @@ "delete_loadedlibrary", "backup", "restore", + "view_identityprovider", + "add_identityprovider", + "change_identityprovider", + "delete_identityprovider", ] diff --git a/backend/core/views.py b/backend/core/views.py index 718040679..02591764a 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -61,6 +61,8 @@ class BaseModelViewSet(viewsets.ModelViewSet): search_fields = ["name", "description"] model: models.Model + serializers_module = "core.serializers" + def get_queryset(self): if not self.model: return None @@ -82,7 +84,7 @@ def get_serializer_class(self): return super().get_serializer_class() # Dynamically import the serializer module and get the serializer class - serializer_module = importlib.import_module("core.serializers") + serializer_module = importlib.import_module(self.serializers_module) serializer_class = getattr(serializer_module, serializer_name) return serializer_class diff --git a/backend/iam/migrations/0004_identityprovider.py b/backend/iam/migrations/0004_identityprovider.py new file mode 100644 index 000000000..f2c147a23 --- /dev/null +++ b/backend/iam/migrations/0004_identityprovider.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.4 on 2024-06-11 17:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'), + ('socialaccount', '0006_alter_socialaccount_extra_data'), + ] + + operations = [ + migrations.CreateModel( + name='IdentityProvider', + fields=[ + ('socialapp_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='socialaccount.socialapp')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + bases=('socialaccount.socialapp',), + ), + ] diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py new file mode 100644 index 000000000..881d98138 --- /dev/null +++ b/backend/iam/sso/models.py @@ -0,0 +1,6 @@ +from allauth.socialaccount import models as socialaccount_models +from django.db import models + + +class IdentityProvider(socialaccount_models.SocialApp): + created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py new file mode 100644 index 000000000..6085b90bf --- /dev/null +++ b/backend/iam/sso/serializers.py @@ -0,0 +1,15 @@ +from .models import IdentityProvider + +from core.serializers import BaseModelSerializer + + +class IdentityProviderReadSerializer(BaseModelSerializer): + class Meta: + model = IdentityProvider + fields = "__all__" + + +class IdentityProviderWriteSerializer(BaseModelSerializer): + class Meta: + model = IdentityProvider + fields = "__all__" diff --git a/backend/iam/sso/urls.py b/backend/iam/sso/urls.py new file mode 100644 index 000000000..4294fcd6c --- /dev/null +++ b/backend/iam/sso/urls.py @@ -0,0 +1,15 @@ +from django.urls import include, path +from rest_framework import routers + +from .views import IdentityProviderViewSet + + +router = routers.DefaultRouter() + +router.register( + r"identity-providers", IdentityProviderViewSet, basename="identity-providers" +) + +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py new file mode 100644 index 000000000..f326af208 --- /dev/null +++ b/backend/iam/sso/views.py @@ -0,0 +1,10 @@ +from core.views import BaseModelViewSet as AbstractBaseModelViewSet +from .models import IdentityProvider + + +class BaseModelViewSet(AbstractBaseModelViewSet): + serializers_module = "iam.sso.serializers" + + +class IdentityProviderViewSet(BaseModelViewSet): + model = IdentityProvider diff --git a/backend/iam/urls.py b/backend/iam/urls.py index ecc222e73..50bc1d212 100644 --- a/backend/iam/urls.py +++ b/backend/iam/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import include, path from .views import ( @@ -24,4 +24,5 @@ name="password-reset-confirm", ), path("set-password/", SetPasswordView.as_view(), name="set-password"), + path("sso/", include("iam.sso.urls")), ] From d7a084214c2c6a7b780206b81c6a1fb2f796de28 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 12 Jun 2024 11:17:59 +0200 Subject: [PATCH 006/115] Put identity-providers endpoints in core urls --- backend/core/urls.py | 4 ++++ backend/core/views.py | 2 +- backend/iam/sso/urls.py | 3 --- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/core/urls.py b/backend/core/urls.py index da0ad92ec..92d32c25b 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -1,3 +1,4 @@ +from iam.sso.views import IdentityProviderViewSet from .views import * from library.views import StoredLibraryViewSet, LoadedLibraryViewSet from iam.sso.saml.views import FinishACSView @@ -42,6 +43,9 @@ ) router.register(r"stored-libraries", StoredLibraryViewSet, basename="stored-libraries") router.register(r"loaded-libraries", LoadedLibraryViewSet, basename="loaded-libraries") +router.register( + r"identity-providers", IdentityProviderViewSet, basename="identity-providers" +) urlpatterns = [ diff --git a/backend/core/views.py b/backend/core/views.py index 02591764a..19dc50ae5 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -128,7 +128,7 @@ class Meta: @action(detail=True, name="Get write data") def object(self, request, pk): serializer_name = f"{self.model.__name__}WriteSerializer" - serializer_module = importlib.import_module("core.serializers") + serializer_module = importlib.import_module(self.serializers_module) serializer_class = getattr(serializer_module, serializer_name) return Response(serializer_class(super().get_object()).data) diff --git a/backend/iam/sso/urls.py b/backend/iam/sso/urls.py index 4294fcd6c..3f4d09dc5 100644 --- a/backend/iam/sso/urls.py +++ b/backend/iam/sso/urls.py @@ -6,9 +6,6 @@ router = routers.DefaultRouter() -router.register( - r"identity-providers", IdentityProviderViewSet, basename="identity-providers" -) urlpatterns = [ path("", include(router.urls)), From 8f41fbbb125a73d024c6d089847db23ba83ae645 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 12 Jun 2024 11:18:25 +0200 Subject: [PATCH 007/115] Rewrite IdentityProvider model --- .../iam/migrations/0004_identityprovider.py | 24 +++++++-- backend/iam/sso/models.py | 52 +++++++++++++++++-- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/backend/iam/migrations/0004_identityprovider.py b/backend/iam/migrations/0004_identityprovider.py index f2c147a23..be45cac3a 100644 --- a/backend/iam/migrations/0004_identityprovider.py +++ b/backend/iam/migrations/0004_identityprovider.py @@ -1,6 +1,8 @@ -# Generated by Django 5.0.4 on 2024-06-11 17:08 +# Generated by Django 5.0.4 on 2024-06-12 09:09 import django.db.models.deletion +import iam.models +import uuid from django.db import migrations, models @@ -8,16 +10,28 @@ class Migration(migrations.Migration): dependencies = [ ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'), - ('socialaccount', '0006_alter_socialaccount_extra_data'), ] operations = [ migrations.CreateModel( name='IdentityProvider', fields=[ - ('socialapp_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='socialaccount.socialapp')), - ('created_at', models.DateTimeField(auto_now_add=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('is_published', models.BooleanField(default=False, verbose_name='published')), + ('provider', models.CharField(max_length=30, verbose_name='provider')), + ('provider_id', models.CharField(blank=True, max_length=200, verbose_name='provider ID')), + ('name', models.CharField(max_length=200, verbose_name='name')), + ('client_id', models.CharField(help_text='App ID, or consumer key', max_length=191, verbose_name='client id')), + ('secret', models.CharField(blank=True, help_text='API secret, client secret, or consumer secret', max_length=191, verbose_name='secret key')), + ('key', models.CharField(blank=True, help_text='Key', max_length=191, verbose_name='key')), + ('settings', models.JSONField(blank=True, default=dict)), + ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')), ], - bases=('socialaccount.socialapp',), + options={ + 'verbose_name': 'identity provider', + 'verbose_name_plural': 'identity providers', + }, ), ] diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py index 881d98138..9faeef83f 100644 --- a/backend/iam/sso/models.py +++ b/backend/iam/sso/models.py @@ -1,6 +1,52 @@ -from allauth.socialaccount import models as socialaccount_models +from allauth.socialaccount.models import providers, SocialAppManager from django.db import models +from core.base_models import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ +from iam.models import FolderMixin -class IdentityProvider(socialaccount_models.SocialApp): - created_at = models.DateTimeField(auto_now_add=True) + +class IdentityProvider(AbstractBaseModel, FolderMixin): + objects = SocialAppManager() + + # The provider type, e.g. "google", "telegram", "saml". + provider = models.CharField( + verbose_name=_("provider"), + max_length=30, + ) + # For providers that support subproviders, such as OpenID Connect and SAML, + # this ID identifies that instance. SocialAccount's originating from app + # will have their `provider` field set to the `provider_id` if available, + # else `provider`. + provider_id = models.CharField( + verbose_name=_("provider ID"), + max_length=200, + blank=True, + ) + name = models.CharField(verbose_name=_("name"), max_length=200) + client_id = models.CharField( + verbose_name=_("client id"), + max_length=191, + help_text=_("App ID, or consumer key"), + ) + secret = models.CharField( + verbose_name=_("secret key"), + max_length=191, + blank=True, + help_text=_("API secret, client secret, or consumer secret"), + ) + key = models.CharField( + verbose_name=_("key"), max_length=191, blank=True, help_text=_("Key") + ) + settings = models.JSONField(default=dict, blank=True) + + class Meta: + verbose_name = _("identity provider") + verbose_name_plural = _("identity providers") + + def __str__(self): + return self.name + + def get_provider(self, request): + provider_class = providers.registry.get_class(self.provider) + return provider_class(request=request, app=self) From fc9725789809d2a2677ee667a4121d118596ed41 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 12 Jun 2024 11:18:41 +0200 Subject: [PATCH 008/115] Allow CRUD operations on identity providers from the frontend --- frontend/messages/en.json | 10 +++++++++- .../src/lib/components/Forms/ModelForm.svelte | 7 +++++++ frontend/src/lib/utils/crud.ts | 19 ++++++++++++++----- frontend/src/lib/utils/locales.ts | 10 +++++++++- frontend/src/lib/utils/schemas.ts | 13 ++++++++++++- frontend/src/lib/utils/table.ts | 4 ++++ frontend/src/lib/utils/types.ts | 3 ++- .../(app)/[model=urlmodel]/+layout.server.ts | 5 +++-- .../(app)/[model=urlmodel]/+page.server.ts | 1 + 9 files changed, 61 insertions(+), 11 deletions(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 51d0364be..292329c87 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -540,5 +540,13 @@ "exportBackupDescription": "This will serialize and create a backup of the database, including users and RBAC. Evidences and other files are not included in the backup.", "importBackupDescription": "This will deserialize and restore the database from a backup. This will overwrite all existing data, including users and RBAC and cannot be undone.", "requirementAppliedControlHelpText": "Evidences linked to the selected measures will be automatically associated with the requirement.", - "requirementEvidenceHelpText": "This tab allows you to add extra evidences to the requirement." + "requirementEvidenceHelpText": "This tab allows you to add extra evidences to the requirement.", + "providerId": "Provider ID", + "clientId": "Client ID", + "secret": "Secret", + "key": "Key", + "settings": "Settings", + "identityProvider": "Identity provider", + "identityProviders": "Identity providers", + "addIdentityProvider": "Add identity provider" } diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index e38cf92d2..f588be194 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -423,6 +423,13 @@ {#if shape.is_active} {/if} + {:else if URLModel === 'identity-providers'} + + + + + +