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'}
+
+
+
+
+
+
{/if}
{#if closeModal}
diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts
index a7d4f2f77..7754a56c7 100644
--- a/frontend/src/lib/utils/crud.ts
+++ b/frontend/src/lib/utils/crud.ts
@@ -53,7 +53,7 @@ export const getOptions = ({
const append = (x, y) => (!y ? x : !x || x == '' ? y : x + ' - ' + y);
const options = objects
.map((object) => {
- let my_label =
+ const my_label =
label != 'auto'
? object[label]
: append(object['ref_id'], object['name'] ? object['name'] : object['description']);
@@ -126,6 +126,7 @@ export interface ModelMapEntry {
reverseForeignKeyFields?: ForeignKeyField[];
selectFields?: SelectField[];
filters?: SelectField[];
+ path?: string;
}
type ModelMap = {
@@ -392,17 +393,25 @@ export const URL_MODEL_MAP: ModelMap = {
},
'stored-libraries': {
name: 'storedlibrary',
- localName: 'stored library',
- localNamePlural: 'stored libraries',
+ localName: 'storedLibrary',
+ localNamePlural: 'storedLibraries',
verboseName: 'stored Library',
verboseNamePlural: 'stored Libraries'
},
'loaded-libraries': {
name: 'loadedlibrary',
- localName: 'loaded library',
- localNamePlural: 'loaded libraries',
+ localName: 'loadedLibrary',
+ localNamePlural: 'loadedLibraries',
verboseName: 'loaded Library',
verboseNamePlural: 'loaded Libraries'
+ },
+ 'identity-providers': {
+ name: 'identityprovider',
+ localName: 'identityProvider',
+ localNamePlural: 'identityProviders',
+ verboseName: 'Identity provider',
+ verboseNamePlural: 'Identity providers',
+ path: 'iam/sso/'
}
};
diff --git a/frontend/src/lib/utils/locales.ts b/frontend/src/lib/utils/locales.ts
index f562d6719..4524e4db5 100644
--- a/frontend/src/lib/utils/locales.ts
+++ b/frontend/src/lib/utils/locales.ts
@@ -188,6 +188,7 @@ export function localItems(languageTag: string): LocalItems {
addProject: m.addProject({ languageTag: languageTag }),
addUser: m.addUser({ languageTag: languageTag }),
addPolicy: m.addPolicy({ languageTag: languageTag }),
+ addIdentityProvider: m.addIdentityProvider({ languageTag: languageTag }),
associatedThreats: m.associatedThreats({ languageTag: languageTag }),
associatedReferenceControls: m.associatedReferenceControls({ languageTag: languageTag }),
associatedAppliedControls: m.associatedAppliedControls({ languageTag: languageTag }),
@@ -365,7 +366,14 @@ export function localItems(languageTag: string): LocalItems {
incoming: m.incoming({ languageTag: languageTag }),
today: m.today({ languageTag: languageTag }),
outdated: m.outdated({ languageTag: languageTag }),
- flashMode: m.flashMode({ languageTag: languageTag })
+ flashMode: m.flashMode({ languageTag: languageTag }),
+ identityProvider: m.identityProvider({ languageTag: languageTag }),
+ identityProviders: m.identityProviders({ languageTag: languageTag }),
+ settings: m.settings({ languageTag: languageTag }),
+ key: m.key({ languageTag: languageTag }),
+ secret: m.secret({ languageTag: languageTag }),
+ clientId: m.clientId({ languageTag: languageTag }),
+ providerId: m.providerId({ languageTag: languageTag })
};
return LOCAL_ITEMS;
}
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index a0abd6e76..fbc922597 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -211,6 +211,16 @@ export const EvidenceSchema = baseNamedObject({
link: z.string().optional().nullable()
});
+export const IdentityProviderSchema = z.object({
+ provider: z.string(),
+ provider_id: z.string().optional(),
+ name: z.string(),
+ client_id: z.string(),
+ secret: z.string().optional(),
+ key: z.string().optional(),
+ settings: z.any().optional()
+});
+
const SCHEMA_MAP: Record
= {
folders: FolderSchema,
projects: ProjectSchema,
@@ -226,7 +236,8 @@ const SCHEMA_MAP: Record = {
'requirement-assessments': RequirementAssessmentSchema,
'compliance-assessments': ComplianceAssessmentSchema,
evidences: EvidenceSchema,
- users: UserCreateSchema
+ users: UserCreateSchema,
+ 'identity-providers': IdentityProviderSchema
};
export const modelSchema = (model: string) => {
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index 23d86e726..dbd7a33e0 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -104,5 +104,9 @@ export const listViewFields = {
'loaded-libraries': {
head: ['ref', 'name', 'description', 'language', 'overview'],
body: ['ref_id', 'name', 'description', 'locale', 'overview']
+ },
+ 'identity-providers': {
+ head: ['provider'],
+ body: ['provider']
}
};
diff --git a/frontend/src/lib/utils/types.ts b/frontend/src/lib/utils/types.ts
index aea58e5a0..929e61845 100644
--- a/frontend/src/lib/utils/types.ts
+++ b/frontend/src/lib/utils/types.ts
@@ -37,7 +37,8 @@ export const URL_MODEL = [
'frameworks',
'requirements',
'requirement-assessments',
- 'libraries'
+ 'libraries',
+ 'identity-providers'
] as const;
export type urlModel = (typeof URL_MODEL)[number];
diff --git a/frontend/src/routes/(app)/[model=urlmodel]/+layout.server.ts b/frontend/src/routes/(app)/[model=urlmodel]/+layout.server.ts
index 7cd8313c2..685d78290 100644
--- a/frontend/src/routes/(app)/[model=urlmodel]/+layout.server.ts
+++ b/frontend/src/routes/(app)/[model=urlmodel]/+layout.server.ts
@@ -2,12 +2,13 @@ import { BASE_API_URL } from '$lib/utils/constants';
import { listViewFields } from '$lib/utils/table';
import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton';
-import { CUSTOM_MODEL_FETCH_MAP } from '$lib/utils/crud';
-import type { urlModel } from '$lib/utils/types';
+import { CUSTOM_MODEL_FETCH_MAP, getModelInfo } from '$lib/utils/crud';
+import type { ModelInfo, urlModel } from '$lib/utils/types';
import type { LayoutServerLoad } from './$types';
export const load = (async ({ fetch, params }) => {
let data = null;
+ const model: ModelInfo = getModelInfo(params.model!);
if (Object.prototype.hasOwnProperty.call(CUSTOM_MODEL_FETCH_MAP, params.model)) {
const fetch_function = CUSTOM_MODEL_FETCH_MAP[params.model];
data = await fetch_function({ fetch, params });
diff --git a/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts b/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts
index 283ce00e8..9d50f61fe 100644
--- a/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts
+++ b/frontend/src/routes/(app)/[model=urlmodel]/+page.server.ts
@@ -81,6 +81,7 @@ export const actions: Actions = {
return fail(400, { form: form });
}
+ const model: ModelInfo = getModelInfo(event.params.model!);
const endpoint = `${BASE_API_URL}/${event.params.model}/`;
const fileFields = Object.fromEntries(
From de65349704ece887952194f9cd004350468d4ebf Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 12 Jun 2024 16:25:34 +0200
Subject: [PATCH 009/115] Improve frontend IdP CRUD
---
backend/ciso_assistant/settings.py | 45 +-----
backend/iam/sso/serializers.py | 134 ++++++++++++++++++
backend/iam/sso/views.py | 8 ++
.../components/Breadcrumbs/Breadcrumbs.svelte | 12 +-
.../src/lib/components/Forms/Checkbox.svelte | 2 +
.../src/lib/components/Forms/ModelForm.svelte | 43 +++++-
.../src/lib/components/Forms/TextField.svelte | 2 +
.../lib/components/Modals/CreateModal.svelte | 2 +-
.../src/lib/components/Modals/Modal.svelte | 8 +-
.../lib/components/SideBar/SideBarItem.svelte | 8 +-
.../src/lib/components/SideBar/navData.ts | 5 +
frontend/src/lib/utils/crud.ts | 2 +-
frontend/src/lib/utils/schemas.ts | 29 +++-
frontend/src/routes/(app)/+layout.svelte | 7 +-
.../(app)/[model=urlmodel]/+page.server.ts | 4 +-
.../(app)/[model=urlmodel]/+page.svelte | 9 +-
.../[model=urlmodel]/[id=uuid]/+page.svelte | 8 +-
.../[id=uuid]/edit/+page.svelte | 1 +
18 files changed, 245 insertions(+), 84 deletions(-)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index d5e4ac509..7b741891b 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -360,50 +360,7 @@ def set_ciso_assistant_url(_, __, event_dict):
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/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index 6085b90bf..5fb236ca9 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -1,4 +1,6 @@
+from rest_framework import serializers
from .models import IdentityProvider
+from pprint import pprint
from core.serializers import BaseModelSerializer
@@ -10,6 +12,138 @@ class Meta:
class IdentityProviderWriteSerializer(BaseModelSerializer):
+ attribute_mapping_uid = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ attribute_mapping_email_verified = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ attribute_mapping_email = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ idp_entity_id = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ metadata_url = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ sso_url = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ slo_url = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ x509cert = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ sp_entity_id = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ allow_repeat_attribute_name = serializers.BooleanField(
+ required=False, write_only=True
+ )
+ allow_single_label_domains = serializers.BooleanField(
+ required=False, write_only=True
+ )
+ authn_request_signed = serializers.BooleanField(required=False, write_only=True)
+ digest_algorithm = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ logout_request_signed = serializers.BooleanField(required=False, write_only=True)
+ logout_response_signed = serializers.BooleanField(required=False, write_only=True)
+ metadata_signed = serializers.BooleanField(required=False, write_only=True)
+ name_id_encrypted = serializers.BooleanField(required=False, write_only=True)
+ reject_deprecated_algorithm = serializers.BooleanField(
+ required=False, write_only=True
+ )
+ reject_idp_initiated_sso = serializers.BooleanField(required=False, write_only=True)
+ signature_algorithm = serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ )
+ want_assertion_encrypted = serializers.BooleanField(required=False, write_only=True)
+ want_assertion_signed = serializers.BooleanField(required=False, write_only=True)
+ want_attribute_statement = serializers.BooleanField(required=False, write_only=True)
+ want_message_signed = serializers.BooleanField(required=False, write_only=True)
+ want_name_id = serializers.BooleanField(required=False, write_only=True)
+ want_name_id_encrypted = serializers.BooleanField(required=False, write_only=True)
+
class Meta:
model = IdentityProvider
fields = "__all__"
+
+ def create(self, validated_data):
+ if validated_data.get("provider") == "saml":
+ settings = {
+ "attribute_mapping": {
+ "uid": validated_data.pop("attribute_mapping_uid", None),
+ "email_verified": validated_data.pop(
+ "attribute_mapping_email_verified", None
+ ),
+ "email": validated_data.pop("attribute_mapping_email", None),
+ },
+ "idp": {
+ "entity_id": validated_data.pop("idp_entity_id", None),
+ "metadata_url": validated_data.pop("metadata_url", None),
+ "sso_url": validated_data.pop("sso_url", None),
+ "slo_url": validated_data.pop("slo_url", None),
+ "x509cert": validated_data.pop("x509cert", None),
+ },
+ "sp": {
+ # Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
+ "entity_id": validated_data.pop("sp_entity_id", None),
+ },
+ # Advanced settings.
+ "advanced": {
+ "allow_repeat_attribute_name": validated_data.pop(
+ "allow_repeat_attribute_name", True
+ ),
+ "allow_single_label_domains": validated_data.pop(
+ "allow_single_label_domains", False
+ ),
+ "authn_request_signed": validated_data.pop(
+ "authn_request_signed", False
+ ),
+ "digest_algorithm": validated_data.pop(
+ "digest_algorithm",
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ ),
+ "logout_request_signed": validated_data.pop(
+ "logout_request_signed", False
+ ),
+ "logout_response_signed": validated_data.pop(
+ "logout_response_signed", False
+ ),
+ "metadata_signed": validated_data.pop("metadata_signed", False),
+ "name_id_encrypted": validated_data.pop("name_id_encrypted", False),
+ "reject_deprecated_algorithm": validated_data.pop(
+ "reject_deprecated_algorithm", True
+ ),
+ # Due to security concerns, IdP initiated SSO is rejected by default.
+ "reject_idp_initiated_sso": validated_data.pop(
+ "reject_idp_initiated_sso", False
+ ),
+ "signature_algorithm": validated_data.pop(
+ "signature_algorithm",
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ ),
+ "want_assertion_encrypted": validated_data.pop(
+ "want_assertion_encrypted", False
+ ),
+ "want_assertion_signed": validated_data.pop(
+ "want_assertion_signed", False
+ ),
+ "want_attribute_statement": validated_data.pop(
+ "want_attribute_statement", True
+ ),
+ "want_message_signed": validated_data.pop(
+ "want_message_signed", False
+ ),
+ "want_name_id": validated_data.pop("want_name_id", False),
+ "want_name_id_encrypted": validated_data.pop(
+ "want_name_id_encrypted", False
+ ),
+ },
+ }
+ validated_data["settings"] = settings
+ pprint(validated_data)
+ return super().create(validated_data)
diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py
index f326af208..230706348 100644
--- a/backend/iam/sso/views.py
+++ b/backend/iam/sso/views.py
@@ -1,5 +1,8 @@
+from rest_framework.response import Response
from core.views import BaseModelViewSet as AbstractBaseModelViewSet
from .models import IdentityProvider
+from rest_framework.decorators import action
+from allauth.socialaccount import providers
class BaseModelViewSet(AbstractBaseModelViewSet):
@@ -8,3 +11,8 @@ class BaseModelViewSet(AbstractBaseModelViewSet):
class IdentityProviderViewSet(BaseModelViewSet):
model = IdentityProvider
+
+ @action(detail=False, name="Get provider choices")
+ def provider(self, request):
+ _providers = providers.registry.as_choices()
+ return Response({p[0]: p[1] for p in _providers})
diff --git a/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte b/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte
index a46e750e3..fa898ce64 100644
--- a/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte
+++ b/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte
@@ -65,8 +65,8 @@
{#if c.icon}
{/if}
- {#if localItems()[c.label]}
- {localItems()[c.label]}
+ {#if Object.hasOwn(m, c.label)}
+ {m[c.label]()}
{:else}
{c.label}
{/if}
@@ -82,8 +82,8 @@
{#if c.icon}
{/if}
- {#if localItems()[c.label]}
- {localItems()[c.label]}
+ {#if Object.hasOwn(m, c.label)}
+ {m[c.label]()}
{:else}
{c.label}
{/if}
@@ -93,8 +93,8 @@
{#if c.icon}
{/if}
- {#if localItems()[c.label]}
- {localItems()[c.label]}
+ {#if Object.hasOwn(m, c.label)}
+ {m[c.label]()}
{:else}
{c.label}
{/if}
diff --git a/frontend/src/lib/components/Forms/Checkbox.svelte b/frontend/src/lib/components/Forms/Checkbox.svelte
index 2dd998f04..35126bec5 100644
--- a/frontend/src/lib/components/Forms/Checkbox.svelte
+++ b/frontend/src/lib/components/Forms/Checkbox.svelte
@@ -5,6 +5,8 @@
export let field: string;
export let helpText: string | undefined = undefined;
+ label = label ?? field;
+
export let form;
const { value, errors, constraints } = formFieldProxy(form, field);
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 4ae68897a..e369332fd 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -426,12 +426,51 @@
{/if}
{:else if URLModel === 'identity-providers'}
-
+
-
+ {#if data.provider === 'saml'}
+ Attribute mapping
+
+
+
+
+ IdP configuration
+
+
+
+
+
+
+ SP configuration
+
+
+ Advanced settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
{/if}
{#if closeModal}
diff --git a/frontend/src/lib/components/Forms/TextField.svelte b/frontend/src/lib/components/Forms/TextField.svelte
index 3d35f75bf..c81cd6de9 100644
--- a/frontend/src/lib/components/Forms/TextField.svelte
+++ b/frontend/src/lib/components/Forms/TextField.svelte
@@ -10,6 +10,8 @@
export let form;
+ label = label ?? field;
+
const { value, errors, constraints } = formFieldProxy(form, field);
$: classesTextField = (errors: string[] | undefined) => (errors ? 'input-error' : '');
diff --git a/frontend/src/lib/components/Modals/CreateModal.svelte b/frontend/src/lib/components/Modals/CreateModal.svelte
index 8d7fe949a..bb3e73f33 100644
--- a/frontend/src/lib/components/Modals/CreateModal.svelte
+++ b/frontend/src/lib/components/Modals/CreateModal.svelte
@@ -42,6 +42,6 @@
-
+
{/if}
diff --git a/frontend/src/lib/components/Modals/Modal.svelte b/frontend/src/lib/components/Modals/Modal.svelte
index 9ffb60d05..467806ff2 100644
--- a/frontend/src/lib/components/Modals/Modal.svelte
+++ b/frontend/src/lib/components/Modals/Modal.svelte
@@ -133,7 +133,7 @@
let registeredInteractionWithBackdrop = false;
let modalElement: HTMLDivElement;
let windowHeight: number;
- let backdropOverflow = 'overflow-y-hidden';
+ let backdropOverflow = 'overflow-y-auto';
const modalStore = getModalStore();
@@ -160,12 +160,6 @@
// modal is closed
if (!modalHeight) return;
-
- if (modalHeight > windowHeight) {
- backdropOverflow = 'overflow-y-auto';
- } else {
- backdropOverflow = 'overflow-y-hidden';
- }
}
// first child of the modal is the content.
$: onModalHeightChange(modalElement);
diff --git a/frontend/src/lib/components/SideBar/SideBarItem.svelte b/frontend/src/lib/components/SideBar/SideBarItem.svelte
index 4c98403af..b0b8cd008 100644
--- a/frontend/src/lib/components/SideBar/SideBarItem.svelte
+++ b/frontend/src/lib/components/SideBar/SideBarItem.svelte
@@ -1,15 +1,9 @@
@@ -106,7 +81,7 @@
Date: Thu, 13 Jun 2024 09:52:58 +0200
Subject: [PATCH 013/115] Remove dead code
---
.../(authentication)/login/+page.svelte | 25 -------------------
1 file changed, 25 deletions(-)
diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte
index 5a5bde0f0..b1a861b6d 100644
--- a/frontend/src/routes/(authentication)/login/+page.svelte
+++ b/frontend/src/routes/(authentication)/login/+page.svelte
@@ -12,31 +12,6 @@
import { redirectToProvider } from '$lib/allauth';
export let data: PageData;
-
- // 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);
- // });
From 43cae5fad65d941af07cc22e7f83b45ea258d0ad Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 11:58:18 +0200
Subject: [PATCH 014/115] Update SocialAccountAdapter to use IdentityProvider
model instead of allauth's SocialApp
---
backend/iam/adapter.py | 84 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 83 insertions(+), 1 deletion(-)
diff --git a/backend/iam/adapter.py b/backend/iam/adapter.py
index c6dc2f762..cbba8bdab 100644
--- a/backend/iam/adapter.py
+++ b/backend/iam/adapter.py
@@ -1,8 +1,14 @@
from allauth.account.utils import perform_login
from allauth.socialaccount.helpers import ImmediateHttpResponse
+from allauth.socialaccount.models import app_settings
from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
-from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
+from allauth.socialaccount.adapter import (
+ DefaultSocialAccountAdapter,
+ MultipleObjectsReturned,
+ warnings,
+)
+from django.db.models import Q
from django.dispatch import receiver
from allauth.socialaccount.signals import pre_social_login
from django.contrib.auth import login, get_user_model
@@ -29,3 +35,79 @@ def pre_social_login(self, request, sociallogin):
return Response(
{"message": "User not found."}, status=HTTP_401_UNAUTHORIZED
)
+
+ def list_apps(self, request, provider=None, client_id=None):
+ """IdentityProvider's can be setup in the database, or, via
+ `settings.SOCIALACCOUNT_PROVIDERS`. This methods returns a uniform list
+ of all known apps matching the specified criteria, and blends both
+ (db/settings) sources of data.
+ """
+ # NOTE: Avoid loading models at top due to registry boot...
+ from .sso.models import IdentityProvider
+
+ # Map provider to the list of apps.
+ provider_to_apps = {}
+
+ # First, populate it with the DB backed apps.
+ db_apps = IdentityProvider.objects.all()
+ if provider:
+ db_apps = db_apps.filter(Q(provider=provider) | Q(provider_id=provider))
+ if client_id:
+ db_apps = db_apps.filter(client_id=client_id)
+ for app in db_apps:
+ apps = provider_to_apps.setdefault(app.provider, [])
+ apps.append(app)
+
+ # Then, extend it with the settings backed apps.
+ for p, pcfg in app_settings.PROVIDERS.items():
+ app_configs = pcfg.get("APPS")
+ if app_configs is None:
+ app_config = pcfg.get("APP")
+ if app_config is None:
+ continue
+ app_configs = [app_config]
+
+ apps = provider_to_apps.setdefault(p, [])
+ for config in app_configs:
+ app = IdentityProvider(provider=p)
+ for field in [
+ "name",
+ "provider_id",
+ "client_id",
+ "secret",
+ "key",
+ "settings",
+ ]:
+ if field in config:
+ setattr(app, field, config[field])
+ if "certificate_key" in config:
+ warnings.warn("'certificate_key' should be moved into app.settings")
+ app.settings["certificate_key"] = config["certificate_key"]
+ if client_id and app.client_id != client_id:
+ continue
+ if (
+ provider
+ and app.provider_id != provider
+ and app.provider != provider
+ ):
+ continue
+ apps.append(app)
+
+ # Flatten the list of apps.
+ apps = []
+ for provider_apps in provider_to_apps.values():
+ apps.extend(provider_apps)
+ return apps
+
+ def get_app(self, request, provider, client_id=None):
+ from .sso.models import IdentityProvider
+
+ apps = self.list_apps(request, provider=provider, client_id=client_id)
+ if len(apps) > 1:
+ visible_apps = [app for app in apps if not app.settings.get("hidden")]
+ if len(visible_apps) != 1:
+ raise MultipleObjectsReturned
+ apps = visible_apps
+ elif len(apps) == 0:
+ raise IdentityProvider.DoesNotExist()
+ return apps[0]
From c80a3421862ad90feb9b97909a13e9a332f09b24 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 11:58:34 +0200
Subject: [PATCH 015/115] Refactor IdentityProviderWriteSerializer
---
backend/iam/sso/serializers.py | 150 +++++++++++++++++----------------
1 file changed, 79 insertions(+), 71 deletions(-)
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index 42b1237dd..3463775f0 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -75,77 +75,85 @@ class Meta:
def create(self, validated_data):
if validated_data.get("provider") == "saml":
- settings = {
- "attribute_mapping": {
- "uid": validated_data.pop("attribute_mapping_uid", None),
- "email_verified": validated_data.pop(
- "attribute_mapping_email_verified", None
- ),
- "email": validated_data.pop("attribute_mapping_email", None),
- },
- "idp": {
- "entity_id": validated_data.pop("idp_entity_id", None),
- "metadata_url": validated_data.pop("metadata_url", None),
- "sso_url": validated_data.pop("sso_url", None),
- "slo_url": validated_data.pop("slo_url", None),
- "x509cert": validated_data.pop("x509cert", None),
- },
- "sp": {
- # Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
- "entity_id": validated_data.pop("sp_entity_id", None),
- },
- # Advanced settings.
- "advanced": {
- "allow_repeat_attribute_name": validated_data.pop(
- "allow_repeat_attribute_name", True
- ),
- "allow_single_label_domains": validated_data.pop(
- "allow_single_label_domains", False
- ),
- "authn_request_signed": validated_data.pop(
- "authn_request_signed", False
- ),
- "digest_algorithm": validated_data.pop(
- "digest_algorithm",
- "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- ),
- "logout_request_signed": validated_data.pop(
- "logout_request_signed", False
- ),
- "logout_response_signed": validated_data.pop(
- "logout_response_signed", False
- ),
- "metadata_signed": validated_data.pop("metadata_signed", False),
- "name_id_encrypted": validated_data.pop("name_id_encrypted", False),
- "reject_deprecated_algorithm": validated_data.pop(
- "reject_deprecated_algorithm", True
- ),
- # Due to security concerns, IdP initiated SSO is rejected by default.
- "reject_idp_initiated_sso": validated_data.pop(
- "reject_idp_initiated_sso", False
- ),
- "signature_algorithm": validated_data.pop(
- "signature_algorithm",
- "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- ),
- "want_assertion_encrypted": validated_data.pop(
- "want_assertion_encrypted", False
- ),
- "want_assertion_signed": validated_data.pop(
- "want_assertion_signed", False
- ),
- "want_attribute_statement": validated_data.pop(
- "want_attribute_statement", True
- ),
- "want_message_signed": validated_data.pop(
- "want_message_signed", False
- ),
- "want_name_id": validated_data.pop("want_name_id", False),
- "want_name_id_encrypted": validated_data.pop(
- "want_name_id_encrypted", False
- ),
- },
- }
+ settings = self.build_saml_settings(validated_data)
validated_data["settings"] = settings
pprint(validated_data)
return super().create(validated_data)
+
+ def update(self, instance, validated_data):
+ if validated_data.get("provider") == "saml":
+ settings = self.build_saml_settings(validated_data)
+ validated_data["settings"] = settings
+ pprint(validated_data)
+ return super().update(instance, validated_data)
+
+ def build_saml_settings(self, validated_data):
+ return {
+ "attribute_mapping": {
+ "uid": validated_data.pop("attribute_mapping_uid", None),
+ "email_verified": validated_data.pop(
+ "attribute_mapping_email_verified", None
+ ),
+ "email": validated_data.pop("attribute_mapping_email", None),
+ },
+ "idp": {
+ "entity_id": validated_data.pop("idp_entity_id", None),
+ "metadata_url": validated_data.pop("metadata_url", None),
+ "sso_url": validated_data.pop("sso_url", None),
+ "slo_url": validated_data.pop("slo_url", None),
+ "x509cert": validated_data.pop("x509cert", ""),
+ },
+ "sp": {
+ # Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
+ "entity_id": validated_data.pop("sp_entity_id", None),
+ },
+ # Advanced settings.
+ "advanced": {
+ "allow_repeat_attribute_name": validated_data.pop(
+ "allow_repeat_attribute_name", True
+ ),
+ "allow_single_label_domains": validated_data.pop(
+ "allow_single_label_domains", False
+ ),
+ "authn_request_signed": validated_data.pop(
+ "authn_request_signed", False
+ ),
+ "digest_algorithm": validated_data.pop(
+ "digest_algorithm",
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ ),
+ "logout_request_signed": validated_data.pop(
+ "logout_request_signed", False
+ ),
+ "logout_response_signed": validated_data.pop(
+ "logout_response_signed", False
+ ),
+ "metadata_signed": validated_data.pop("metadata_signed", False),
+ "name_id_encrypted": validated_data.pop("name_id_encrypted", False),
+ "reject_deprecated_algorithm": validated_data.pop(
+ "reject_deprecated_algorithm", True
+ ),
+ # Due to security concerns, IdP initiated SSO is rejected by default.
+ "reject_idp_initiated_sso": validated_data.pop(
+ "reject_idp_initiated_sso", False
+ ),
+ "signature_algorithm": validated_data.pop(
+ "signature_algorithm",
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ ),
+ "want_assertion_encrypted": validated_data.pop(
+ "want_assertion_encrypted", False
+ ),
+ "want_assertion_signed": validated_data.pop(
+ "want_assertion_signed", False
+ ),
+ "want_attribute_statement": validated_data.pop(
+ "want_attribute_statement", True
+ ),
+ "want_message_signed": validated_data.pop("want_message_signed", False),
+ "want_name_id": validated_data.pop("want_name_id", False),
+ "want_name_id_encrypted": validated_data.pop(
+ "want_name_id_encrypted", False
+ ),
+ },
+ }
From c6ab52cf06b8ff350ff8dd817518baecd68bb1a7 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 11:58:51 +0200
Subject: [PATCH 016/115] Use TextArea for x509cert field
---
frontend/src/lib/components/Forms/ModelForm.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index e369332fd..62d2b7c70 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -447,7 +447,7 @@
-
+
SP configuration
From c700f2a792161d8493009721b28feda1df5a5e19 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 11:58:59 +0200
Subject: [PATCH 017/115] Update IdentityProviderSchema
---
frontend/src/lib/utils/schemas.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index f8409252c..49b7a787b 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -224,9 +224,9 @@ export const IdentityProviderSchema = z.object({
attribute_mapping_email_verified: z.string().optional(),
attribute_mapping_email: z.string().optional(),
idp_entity_id: z.string().optional(),
- metadata_url: z.string().optional(),
- sso_url: z.string().optional(),
- slo_url: z.string().optional(),
+ metadata_url: z.string().url().optional(),
+ sso_url: z.string().url().optional(),
+ slo_url: z.string().url().optional(),
x509cert: z.string().optional(),
sp_entity_id: z.string().optional(),
allow_repeat_attribute_name: z.boolean().default(true),
From 487a4fdfe51cc837a075f2c949de17e48e2fb015 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 11:59:06 +0200
Subject: [PATCH 018/115] Update identity provider ModelTable
---
frontend/src/lib/utils/table.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index 6b5a7ace2..3e0e385cf 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -106,7 +106,7 @@ export const listViewFields = {
body: ['ref_id', 'name', 'description', 'locale', 'overview']
},
'identity-providers': {
- head: ['name', 'provider'],
- body: ['name', 'provider']
+ head: ['name', 'provider', 'providerId'],
+ body: ['name', 'provider', 'provider_id']
}
};
From 0a1c7aed463d2f56ea67a7d5328e7af240c8c3e0 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 11:59:43 +0200
Subject: [PATCH 019/115] Properly display localized table headins in
ModelTable
---
frontend/src/lib/components/ModelTable/ModelTable.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/lib/components/ModelTable/ModelTable.svelte b/frontend/src/lib/components/ModelTable/ModelTable.svelte
index decd490af..126a2781f 100644
--- a/frontend/src/lib/components/ModelTable/ModelTable.svelte
+++ b/frontend/src/lib/components/ModelTable/ModelTable.svelte
@@ -165,7 +165,7 @@
{#each Object.entries(source.head) as [key, heading]}
- {localItems()[heading]} |
+ {m[heading]() ?? heading} |
{/each}
{#if displayActions}
|
From b0048562a84094b9156c10b1185f75e05491bee2 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 12:00:35 +0200
Subject: [PATCH 020/115] Display fallback label on TextArea
---
frontend/src/lib/components/Forms/TextArea.svelte | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frontend/src/lib/components/Forms/TextArea.svelte b/frontend/src/lib/components/Forms/TextArea.svelte
index 07b48813f..80ddf9178 100644
--- a/frontend/src/lib/components/Forms/TextArea.svelte
+++ b/frontend/src/lib/components/Forms/TextArea.svelte
@@ -11,6 +11,8 @@
export let helpText: string | undefined = undefined;
export let form;
+ label = label ?? field;
+
const { value, errors, constraints } = formFieldProxy(form, field);
$: classesTextField = (errors: string[] | undefined) => (errors ? 'input-error' : '');
From 39f47e4001e2abc74ae1fcbe86efa00412a1c6b7 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 13 Jun 2024 16:03:39 +0200
Subject: [PATCH 021/115] feat: add lxml dependency
---
backend/requirements.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index b7cca8bb7..393d31fab 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -19,4 +19,5 @@ 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
+django-allauth[socialaccount]>=0.63.3
+lxml>=4.6.5,!=4.7.0,<=5.2.1
\ No newline at end of file
From f95c4eb2c686208aef6a52a17a8eb09c12aaba71 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 16:42:33 +0200
Subject: [PATCH 022/115] Properly get email address from SAML response
---
backend/iam/sso/saml/views.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/backend/iam/sso/saml/views.py b/backend/iam/sso/saml/views.py
index bc20317a7..634a82feb 100644
--- a/backend/iam/sso/saml/views.py
+++ b/backend/iam/sso/saml/views.py
@@ -19,8 +19,6 @@
from django.views import View
from rest_framework.views import csrf_exempt
-from pprint import pprint
-
import structlog
from iam.models import User
@@ -108,8 +106,7 @@ def dispatch(self, request, organization_slug):
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]
+ email = auth._nameid
user = User.objects.get(email=email)
token = generate_token(user)
login.state["next"] += f"sso/authenticate/{token}"
From 0278e90f9ffd61a8206bdf5d901199977a650aa9 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 16:42:53 +0200
Subject: [PATCH 023/115] Update IdentityProvider serializers
---
backend/iam/sso/serializers.py | 57 ++++++++++++++++++++++------------
1 file changed, 38 insertions(+), 19 deletions(-)
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index 3463775f0..e08898239 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -1,12 +1,13 @@
+from allauth.socialaccount.providers.saml.provider import SAMLProvider
from rest_framework import serializers
from .models import IdentityProvider
-from pprint import pprint
from core.serializers import BaseModelSerializer
class IdentityProviderReadSerializer(BaseModelSerializer):
provider = serializers.CharField(read_only=True, source="get_provider_display")
+ settings = serializers.CharField(read_only=True)
class Meta:
model = IdentityProvider
@@ -14,14 +15,29 @@ class Meta:
class IdentityProviderWriteSerializer(BaseModelSerializer):
- attribute_mapping_uid = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ attribute_mapping_uid = serializers.ListField(
+ child=serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ ),
+ write_only=True,
+ required=False,
+ allow_null=True,
)
- attribute_mapping_email_verified = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ attribute_mapping_email_verified = serializers.ListField(
+ child=serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ ),
+ write_only=True,
+ required=False,
+ allow_null=True,
)
- attribute_mapping_email = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ attribute_mapping_email = serializers.ListField(
+ child=serializers.CharField(
+ required=False, allow_blank=True, allow_null=True, write_only=True
+ ),
+ write_only=True,
+ required=False,
+ allow_null=True,
)
idp_entity_id = serializers.CharField(
required=False, allow_blank=True, allow_null=True, write_only=True
@@ -77,35 +93,38 @@ def create(self, validated_data):
if validated_data.get("provider") == "saml":
settings = self.build_saml_settings(validated_data)
validated_data["settings"] = settings
- pprint(validated_data)
return super().create(validated_data)
def update(self, instance, validated_data):
if validated_data.get("provider") == "saml":
settings = self.build_saml_settings(validated_data)
validated_data["settings"] = settings
- pprint(validated_data)
return super().update(instance, validated_data)
def build_saml_settings(self, validated_data):
+ default_attribute_mapping = SAMLProvider.default_attribute_mapping
+ attribute_mapping = {
+ "uid": validated_data.pop("attribute_mapping_uid", None),
+ "email_verified": validated_data.pop(
+ "attribute_mapping_email_verified", None
+ ),
+ "email": validated_data.pop("attribute_mapping_email", None),
+ }
return {
"attribute_mapping": {
- "uid": validated_data.pop("attribute_mapping_uid", None),
- "email_verified": validated_data.pop(
- "attribute_mapping_email_verified", None
- ),
- "email": validated_data.pop("attribute_mapping_email", None),
+ key: value if value is not None else default_attribute_mapping[key]
+ for key, value in attribute_mapping.items()
},
"idp": {
- "entity_id": validated_data.pop("idp_entity_id", None),
- "metadata_url": validated_data.pop("metadata_url", None),
- "sso_url": validated_data.pop("sso_url", None),
- "slo_url": validated_data.pop("slo_url", None),
+ "entity_id": validated_data.pop("idp_entity_id", ""),
+ "metadata_url": validated_data.pop("metadata_url", ""),
+ "sso_url": validated_data.pop("sso_url", ""),
+ "slo_url": validated_data.pop("slo_url", ""),
"x509cert": validated_data.pop("x509cert", ""),
},
"sp": {
# Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
- "entity_id": validated_data.pop("sp_entity_id", None),
+ "entity_id": validated_data.pop("sp_entity_id", ""),
},
# Advanced settings.
"advanced": {
From ca923f0a7afc8f9b2faf42087eac063b0f6d2f7a Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 16:43:07 +0200
Subject: [PATCH 024/115] Update IdentityProviderSchema
---
frontend/src/lib/utils/schemas.ts | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index 49b7a787b..cc0163fc6 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -220,9 +220,15 @@ export const IdentityProviderSchema = z.object({
key: z.string().optional(),
// SAML specific fields
- attribute_mapping_uid: z.string().optional(),
- attribute_mapping_email_verified: z.string().optional(),
- attribute_mapping_email: z.string().optional(),
+ attribute_mapping_uid: z
+ .preprocess(toArrayPreprocessor, z.array(z.string().optional()))
+ .optional(),
+ attribute_mapping_email_verified: z
+ .preprocess(toArrayPreprocessor, z.array(z.string().optional()))
+ .optional(),
+ attribute_mapping_email: z
+ .preprocess(toArrayPreprocessor, z.array(z.string().optional()))
+ .optional(),
idp_entity_id: z.string().optional(),
metadata_url: z.string().url().optional(),
sso_url: z.string().url().optional(),
From 71ad06e7959fb3b30a5965f8627347095e21fbf7 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 13 Jun 2024 16:43:35 +0200
Subject: [PATCH 025/115] Use m translation instead of localItems
---
.../(app)/[model=urlmodel]/[id=uuid]/edit/+page.server.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.server.ts b/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.server.ts
index d90fa25d6..d5d6fc969 100644
--- a/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.server.ts
+++ b/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.server.ts
@@ -94,7 +94,7 @@ export const actions: Actions = {
{
type: 'success',
message: m.successfullyUpdatedObject({
- object: localItems()[toCamelCase(modelVerboseName.toLowerCase())].toLowerCase()
+ object: m[toCamelCase(modelVerboseName.toLowerCase())]().toLowerCase()
})
},
event
From 0390b617b43e1cee3a85ebcbacca2caa747ad655 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 13 Jun 2024 21:59:00 +0200
Subject: [PATCH 026/115] feat: handle sso failure
---
backend/iam/sso/saml/views.py | 5 ++++-
frontend/messages/en.json | 3 ++-
frontend/src/hooks.server.ts | 8 ++++++++
3 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/backend/iam/sso/saml/views.py b/backend/iam/sso/saml/views.py
index f48fe8ca4..da811585f 100644
--- a/backend/iam/sso/saml/views.py
+++ b/backend/iam/sso/saml/views.py
@@ -110,7 +110,10 @@ def dispatch(self, request, organization_slug):
login.state["next"] = next_url
print("LOGIN STATE", login.state)
email = auth._nameid
- user = User.objects.get(email=email)
+ try:
+ user = User.objects.get(email=email)
+ except:
+ return render_authentication_error(request, provider, error="failedSSO")
token = generate_token(user)
login.state["next"] += f"sso/authenticate/{token}"
return complete_social_login(request, login)
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 51d0364be..150e80c39 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -540,5 +540,6 @@
"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.",
+ "failedSSO": "Failed to authenticate with SSO, please contact your administrator"
}
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts
index a9380d8f0..d7088b7bd 100644
--- a/frontend/src/hooks.server.ts
+++ b/frontend/src/hooks.server.ts
@@ -1,6 +1,8 @@
import { BASE_API_URL } from '$lib/utils/constants';
import type { User } from '$lib/utils/types';
import { redirect, type Handle, type RequestEvent, type HandleFetch } from '@sveltejs/kit';
+import { setFlash } from 'sveltekit-flash-message/server';
+import * as m from '$paraglide/messages';
async function ensureCsrfToken(event: RequestEvent): Promise {
let csrfToken = event.cookies.get('csrftoken') || '';
@@ -47,6 +49,12 @@ export const handle: Handle = async ({ event, resolve }) => {
if (event.locals.user) return await resolve(event);
+ const errorId = new URL(event.request.url).searchParams.get('error');
+ if (errorId) {
+ setFlash({ type: 'error', message: m.failedSSO() }, event);
+ redirect(302, `/`);
+ }
+
const user = await validateUserSession(event);
if (user) {
event.locals.user = user;
From 67c8f891daabee96e769a4357a8124054e08f33d Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 13 Jun 2024 22:23:58 +0200
Subject: [PATCH 027/115] style: improve sso button
---
frontend/messages/en.json | 4 +++-
frontend/src/routes/(authentication)/login/+page.svelte | 8 +++++++-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index d09fac1da..79e835b18 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -562,5 +562,7 @@
"evidenceNoFile": "Evidence has no file uploaded",
"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.",
- "failedSSO": "Failed to authenticate with SSO, please contact your administrator"
+ "failedSSO": "Failed to authenticate with SSO, please contact your administrator",
+ "loginSSO": "Login with SSO",
+ "or": "or"
}
diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte
index d2353317a..9181b1d3e 100644
--- a/frontend/src/routes/(authentication)/login/+page.svelte
+++ b/frontend/src/routes/(authentication)/login/+page.svelte
@@ -78,13 +78,19 @@
+
+
+ {m.or()}
+
+
{m.loginSSO()}
From 26ccce12828bfbd3ca6301f798fddb80bb325a87 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene <90701924+Mohamed-Hacene@users.noreply.github.com>
Date: Thu, 13 Jun 2024 22:26:58 +0200
Subject: [PATCH 028/115] =?UTF-8?q?chore:=20update=20translations=20with?=
=?UTF-8?q?=20Fink=20=F0=9F=90=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/messages/de.json | 6 +++---
frontend/messages/en.json | 4 +++-
frontend/messages/es.json | 12 ++++++------
frontend/messages/fr.json | 4 +++-
frontend/messages/it.json | 6 +++---
frontend/messages/nl.json | 6 +++---
frontend/messages/pt.json | 4 +++-
7 files changed, 24 insertions(+), 18 deletions(-)
diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index 3b61fd3ba..cf21b2510 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -474,7 +474,6 @@
"attachmentDeleted": "Der Anhang wurde erfolgreich gelöscht",
"librarySuccessfullyLoaded": "Die Bibliothek wurde erfolgreich geladen",
"noLibraryDetected": "Keine Bibliothek erkannt",
- "errorImportingLibrary": "Fehler beim Importieren der Bibliothek",
"passwordSuccessfullyChanged": "Ihr Passwort wurde erfolgreich geändert",
"passwordSuccessfullyReset": "Ihr Passwort wurde erfolgreich zurückgesetzt",
"passwordSuccessfullySet": "Ihr Passwort wurde erfolgreich festgelegt",
@@ -498,7 +497,6 @@
"lowSOK": "Die Wissensstärke zur Unterstützung der Bewertung ist niedrig",
"mediumSOK": "Die Wissensstärke zur Unterstützung der Bewertung ist mittel",
"highSOK": "Die Wissensstärke zur Unterstützung der Bewertung ist hoch",
- "libraryImportError": "Beim Importieren Ihrer Bibliothek ist ein Fehler aufgetreten.",
"libraryAlreadyLoadedError": "Diese Bibliothek wurde bereits geladen.",
"invalidLibraryFileError": "Ungültige Bibliotheksdatei. Stellen Sie sicher, dass das Format korrekt ist.",
"taintedFormMessage": "Möchten Sie diese Seite verlassen? Änderungen, die Sie vorgenommen haben, werden möglicherweise nicht gespeichert.",
@@ -554,5 +552,7 @@
"appliedControlNoReferenceControl": "Für die angewandte Steuerung ist keine Referenzsteuerung ausgewählt",
"evidenceNoFile": "Für den Beweis wurde keine Datei hochgeladen",
"requirementAppliedControlHelpText": "Mit den ausgewählten Maßnahmen verknüpfte Nachweise werden automatisch der Anforderung zugeordnet.",
- "requirementEvidenceHelpText": "Über diese Registerkarte können Sie der Anforderung weitere Nachweise hinzufügen."
+ "requirementEvidenceHelpText": "Über diese Registerkarte können Sie der Anforderung weitere Nachweise hinzufügen.",
+ "errorImportingLibrary": "Fehler beim Importieren der Bibliothek",
+ "libraryImportError": "Beim Importieren Ihrer Bibliothek ist ein Fehler aufgetreten."
}
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 79e835b18..1c6924219 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -564,5 +564,7 @@
"requirementEvidenceHelpText": "This tab allows you to add extra evidences to the requirement.",
"failedSSO": "Failed to authenticate with SSO, please contact your administrator",
"loginSSO": "Login with SSO",
- "or": "or"
+ "or": "or",
+ "errorImportingLibrary": "Error during library import",
+ "libraryImportError": "An error occurred during library import"
}
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index 45ebb8522..8799453d9 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -474,7 +474,6 @@
"attachmentDeleted": "El adjunto se ha eliminado con éxito",
"librarySuccessfullyLoaded": "La biblioteca se ha cargado con éxito",
"noLibraryDetected": "No se detectó ninguna biblioteca",
- "errorImportingLibrary": "Error al importar la biblioteca",
"passwordSuccessfullyChanged": "Su contraseña se ha cambiado con éxito",
"passwordSuccessfullyReset": "Su contraseña se ha restablecido con éxito",
"passwordSuccessfullySet": "Su contraseña se ha establecido con éxito",
@@ -498,7 +497,6 @@
"lowSOK": "La fortaleza del conocimiento que respalda la evaluación es baja",
"mediumSOK": "La fortaleza del conocimiento que respalda la evaluación es media",
"highSOK": "La fortaleza del conocimiento que respalda la evaluación es alta",
- "libraryImportError": "Ocurrió un error durante la importación de su biblioteca.",
"libraryAlreadyLoadedError": "Esta biblioteca ya está cargada.",
"invalidLibraryFileError": "Archivo de biblioteca no válido. Asegúrese de que el formato sea correcto.",
"taintedFormMessage": "¿Desea abandonar esta página? Es posible que no se guarden los cambios que haya realizado.",
@@ -529,6 +527,9 @@
"asZIP": "como ZIP",
"incoming": "Entrante",
"outdated": "Desactualizado",
+ "goBackToAudit": "Volver a la auditorÃa",
+ "exportBackupDescription": "Esto serializará y creará una copia de seguridad de la base de datos, incluidos los usuarios y RBAC. Las pruebas y otros archivos no se incluyen en la copia de seguridad.",
+ "importBackupDescription": "Esto deserializará y restaurará la base de datos desde una copia de seguridad. Esto sobrescribirá todos los datos existentes, incluidos los usuarios y RBAC, y no se puede deshacer.",
"riskAssessmentInProgress": "La evaluación de riesgos aún está en progreso",
"riskAssessmentNoAuthor": "Ningún autor asignado a esta evaluación de riesgos",
"riskAssessmentEmpty": "La evaluación de riesgos está vacÃa. Aún no se ha declarado ningún escenario de riesgo",
@@ -550,9 +551,8 @@
"requirementAssessmentNoAppliedControl": "El estado de la evaluación de requisitos es conforme o parcialmente conforme sin que se haya aplicado ningún control.",
"appliedControlNoReferenceControl": "El control aplicado no tiene ningún control de referencia seleccionado",
"evidenceNoFile": "La evidencia no tiene ningún archivo subido",
- "goBackToAudit": "Volver a la auditorÃa",
- "exportBackupDescription": "Esto serializará y creará una copia de seguridad de la base de datos, incluidos los usuarios y RBAC. Las pruebas y otros archivos no se incluyen en la copia de seguridad.",
- "importBackupDescription": "Esto deserializará y restaurará la base de datos desde una copia de seguridad. Esto sobrescribirá todos los datos existentes, incluidos los usuarios y RBAC, y no se puede deshacer.",
"requirementAppliedControlHelpText": "Las evidencias vinculadas a las medidas seleccionadas se asociarán automáticamente al requisito.",
- "requirementEvidenceHelpText": "Esta pestaña le permite agregar evidencias adicionales al requisito."
+ "requirementEvidenceHelpText": "Esta pestaña le permite agregar evidencias adicionales al requisito.",
+ "errorImportingLibrary": "Error al importar la biblioteca",
+ "libraryImportError": "Ocurrió un error durante la importación de su biblioteca."
}
diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json
index 1123a1729..c5694ca4c 100644
--- a/frontend/messages/fr.json
+++ b/frontend/messages/fr.json
@@ -560,5 +560,7 @@
"appliedControlNoReferenceControl": "La mesure appliquée n'a aucune mesure de référence sélectionnée",
"evidenceNoFile": "Aucun fichier n'a été téléchargé pour les preuves",
"requirementAppliedControlHelpText": "Les preuves liées aux mesures sélectionnées seront automatiquement associées à l'exigence.",
- "requirementEvidenceHelpText": "Cet onglet vous permet d'ajouter des preuves supplémentaires à l'exigence."
+ "requirementEvidenceHelpText": "Cet onglet vous permet d'ajouter des preuves supplémentaires à l'exigence.",
+ "errorImportingLibrary": "Erreur lors de l'importation de la bibliothèque",
+ "libraryImportError": "Une erreur s'est produite lors de l'importation de la bibliothèque"
}
diff --git a/frontend/messages/it.json b/frontend/messages/it.json
index a74c6189a..9ed5622e9 100644
--- a/frontend/messages/it.json
+++ b/frontend/messages/it.json
@@ -474,7 +474,6 @@
"attachmentDeleted": "L'allegato è stato eliminato con successo",
"librarySuccessfullyLoaded": "La biblioteca è stata caricata con successo",
"noLibraryDetected": "Nessuna biblioteca rilevata",
- "errorImportingLibrary": "Errore durante l'importazione della biblioteca",
"passwordSuccessfullyChanged": "La tua password è stata cambiata con successo",
"passwordSuccessfullyReset": "La tua password è stata reimpostata con successo",
"passwordSuccessfullySet": "La tua password è stata impostata con successo",
@@ -498,7 +497,6 @@
"lowSOK": "La forza della conoscenza che supporta la valutazione è bassa",
"mediumSOK": "La forza della conoscenza che supporta la valutazione è media",
"highSOK": "La forza della conoscenza che supporta la valutazione è alta",
- "libraryImportError": "Si è verificato un errore durante l'importazione della tua biblioteca.",
"libraryAlreadyLoadedError": "Questa biblioteca è già stata caricata.",
"invalidLibraryFileError": "File di biblioteca non valido. Assicurati che il formato sia corretto.",
"taintedFormMessage": "Vuoi lasciare questa pagina? Le modifiche apportate potrebbero non essere salvate.",
@@ -554,5 +552,7 @@
"appliedControlNoReferenceControl": "Per il controllo applicato non è selezionato alcun controllo di riferimento",
"evidenceNoFile": "Nessun file è stato caricato nelle prove",
"requirementAppliedControlHelpText": "Le evidenze legate alle misure selezionate verranno automaticamente associate al requisito.",
- "requirementEvidenceHelpText": "Questa scheda ti consente di aggiungere ulteriori prove al requisito."
+ "requirementEvidenceHelpText": "Questa scheda ti consente di aggiungere ulteriori prove al requisito.",
+ "errorImportingLibrary": "Errore durante l'importazione della biblioteca",
+ "libraryImportError": "Si è verificato un errore durante l'importazione della tua biblioteca."
}
diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json
index db33451f4..95aac26c7 100644
--- a/frontend/messages/nl.json
+++ b/frontend/messages/nl.json
@@ -474,7 +474,6 @@
"attachmentDeleted": "De bijlage is succesvol verwijderd",
"librarySuccessfullyLoaded": "De bibliotheek is succesvol geladen",
"noLibraryDetected": "Geen bibliotheek gedetecteerd",
- "errorImportingLibrary": "Fout bij het importeren van de bibliotheek",
"passwordSuccessfullyChanged": "Je wachtwoord is succesvol gewijzigd",
"passwordSuccessfullyReset": "Je wachtwoord is succesvol gereset",
"passwordSuccessfullySet": "Je wachtwoord is succesvol ingesteld",
@@ -498,7 +497,6 @@
"lowSOK": "De sterkte van de kennis die de beoordeling ondersteunt is laag",
"mediumSOK": "De sterkte van de kennis die de beoordeling ondersteunt is medium",
"highSOK": "De sterkte van de kennis die de beoordeling ondersteunt is hoog",
- "libraryImportError": "Er is een fout opgetreden tijdens het importeren van je bibliotheek.",
"libraryAlreadyLoadedError": "Deze bibliotheek is al geladen.",
"invalidLibraryFileError": "Ongeldig bibliotheekbestand. Zorg ervoor dat het formaat correct is.",
"taintedFormMessage": "Wil je deze pagina verlaten? Wijzigingen die je hebt aangebracht, worden mogelijk niet opgeslagen.",
@@ -554,5 +552,7 @@
"appliedControlNoReferenceControl": "Voor de toegepaste regeling is geen referentieregeling geselecteerd",
"evidenceNoFile": "Er is geen bestand geüpload voor bewijsmateriaal",
"requirementAppliedControlHelpText": "Bewijsstukken die verband houden met de geselecteerde maatregelen worden automatisch aan de eis gekoppeld.",
- "requirementEvidenceHelpText": "Op dit tabblad kunt u extra bewijsstukken aan de eis toevoegen."
+ "requirementEvidenceHelpText": "Op dit tabblad kunt u extra bewijsstukken aan de eis toevoegen.",
+ "errorImportingLibrary": "Fout bij het importeren van de bibliotheek",
+ "libraryImportError": "Er is een fout opgetreden tijdens het importeren van je bibliotheek."
}
diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json
index 9b9f91a0c..d1812e8bc 100644
--- a/frontend/messages/pt.json
+++ b/frontend/messages/pt.json
@@ -560,5 +560,7 @@
"appliedControlNoReferenceControl": "O controle aplicado não tem nenhum controle de referência selecionado",
"evidenceNoFile": "A evidência não tem nenhum arquivo carregado",
"requirementAppliedControlHelpText": "As evidências vinculadas às medidas selecionadas serão automaticamente associadas ao requisito.",
- "requirementEvidenceHelpText": "Esta aba permite adicionar evidências extras ao requisito."
+ "requirementEvidenceHelpText": "Esta aba permite adicionar evidências extras ao requisito.",
+ "errorImportingLibrary": "Erro durante a importação da biblioteca",
+ "libraryImportError": "Ocorreu um erro durante a importação da biblioteca"
}
From 6ad7bc2145f8eeb727a6d2c130aef0a7d1a04c0a Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene <90701924+Mohamed-Hacene@users.noreply.github.com>
Date: Thu, 13 Jun 2024 22:30:13 +0200
Subject: [PATCH 029/115] =?UTF-8?q?chore:=20update=20translations=20with?=
=?UTF-8?q?=20Fink=20=F0=9F=90=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/messages/de.json | 3 +++
frontend/messages/en.json | 2 +-
frontend/messages/es.json | 3 +++
frontend/messages/fr.json | 3 +++
frontend/messages/it.json | 3 +++
frontend/messages/nl.json | 3 +++
frontend/messages/pt.json | 3 +++
7 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index cf21b2510..e4e673543 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -553,6 +553,9 @@
"evidenceNoFile": "Für den Beweis wurde keine Datei hochgeladen",
"requirementAppliedControlHelpText": "Mit den ausgewählten Maßnahmen verknüpfte Nachweise werden automatisch der Anforderung zugeordnet.",
"requirementEvidenceHelpText": "Über diese Registerkarte können Sie der Anforderung weitere Nachweise hinzufügen.",
+ "failedSSO": "SSO-Authentifizierung fehlgeschlagen, bitte wenden Sie sich an Ihren Administrator",
+ "loginSSO": "Melden Sie sich bei SSO an",
+ "or": "oder",
"errorImportingLibrary": "Fehler beim Importieren der Bibliothek",
"libraryImportError": "Beim Importieren Ihrer Bibliothek ist ein Fehler aufgetreten."
}
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 1c6924219..9e2d46184 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -562,7 +562,7 @@
"evidenceNoFile": "Evidence has no file uploaded",
"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.",
- "failedSSO": "Failed to authenticate with SSO, please contact your administrator",
+ "failedSSO": "SSO authentication failed, please contact your administrator",
"loginSSO": "Login with SSO",
"or": "or",
"errorImportingLibrary": "Error during library import",
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index 8799453d9..a8227fcf5 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -553,6 +553,9 @@
"evidenceNoFile": "La evidencia no tiene ningún archivo subido",
"requirementAppliedControlHelpText": "Las evidencias vinculadas a las medidas seleccionadas se asociarán automáticamente al requisito.",
"requirementEvidenceHelpText": "Esta pestaña le permite agregar evidencias adicionales al requisito.",
+ "failedSSO": "La autenticación SSO falló; comunÃquese con su administrador",
+ "loginSSO": "Inicie sesión en SSO",
+ "or": "o",
"errorImportingLibrary": "Error al importar la biblioteca",
"libraryImportError": "Ocurrió un error durante la importación de su biblioteca."
}
diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json
index c5694ca4c..0fcf3bff4 100644
--- a/frontend/messages/fr.json
+++ b/frontend/messages/fr.json
@@ -561,6 +561,9 @@
"evidenceNoFile": "Aucun fichier n'a été téléchargé pour les preuves",
"requirementAppliedControlHelpText": "Les preuves liées aux mesures sélectionnées seront automatiquement associées à l'exigence.",
"requirementEvidenceHelpText": "Cet onglet vous permet d'ajouter des preuves supplémentaires à l'exigence.",
+ "failedSSO": "L'authentification SSO a échoué, veuillez contacter votre administrateur",
+ "loginSSO": "Connectez-vous en SSO",
+ "or": "ou",
"errorImportingLibrary": "Erreur lors de l'importation de la bibliothèque",
"libraryImportError": "Une erreur s'est produite lors de l'importation de la bibliothèque"
}
diff --git a/frontend/messages/it.json b/frontend/messages/it.json
index 9ed5622e9..3c449d0ce 100644
--- a/frontend/messages/it.json
+++ b/frontend/messages/it.json
@@ -553,6 +553,9 @@
"evidenceNoFile": "Nessun file è stato caricato nelle prove",
"requirementAppliedControlHelpText": "Le evidenze legate alle misure selezionate verranno automaticamente associate al requisito.",
"requirementEvidenceHelpText": "Questa scheda ti consente di aggiungere ulteriori prove al requisito.",
+ "failedSSO": "Autenticazione SSO non riuscita, contatta il tuo amministratore",
+ "loginSSO": "Accedi a SSO",
+ "or": "O",
"errorImportingLibrary": "Errore durante l'importazione della biblioteca",
"libraryImportError": "Si è verificato un errore durante l'importazione della tua biblioteca."
}
diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json
index 95aac26c7..d9d59e00f 100644
--- a/frontend/messages/nl.json
+++ b/frontend/messages/nl.json
@@ -553,6 +553,9 @@
"evidenceNoFile": "Er is geen bestand geüpload voor bewijsmateriaal",
"requirementAppliedControlHelpText": "Bewijsstukken die verband houden met de geselecteerde maatregelen worden automatisch aan de eis gekoppeld.",
"requirementEvidenceHelpText": "Op dit tabblad kunt u extra bewijsstukken aan de eis toevoegen.",
+ "failedSSO": "SSO-authenticatie mislukt. Neem contact op met uw beheerder",
+ "loginSSO": "Log in op SSO",
+ "or": "of",
"errorImportingLibrary": "Fout bij het importeren van de bibliotheek",
"libraryImportError": "Er is een fout opgetreden tijdens het importeren van je bibliotheek."
}
diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json
index d1812e8bc..e6ac3f15b 100644
--- a/frontend/messages/pt.json
+++ b/frontend/messages/pt.json
@@ -561,6 +561,9 @@
"evidenceNoFile": "A evidência não tem nenhum arquivo carregado",
"requirementAppliedControlHelpText": "As evidências vinculadas às medidas selecionadas serão automaticamente associadas ao requisito.",
"requirementEvidenceHelpText": "Esta aba permite adicionar evidências extras ao requisito.",
+ "failedSSO": "Falha na autenticação SSO. Entre em contato com seu administrador",
+ "loginSSO": "Faça login no SSO",
+ "or": "ou",
"errorImportingLibrary": "Erro durante a importação da biblioteca",
"libraryImportError": "Ocorreu um erro durante a importação da biblioteca"
}
From 4836cb0474c063723531c5ce369d9b84aadba74e Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 13 Jun 2024 22:39:25 +0200
Subject: [PATCH 030/115] fix: synchronize language
---
frontend/src/hooks.server.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts
index d7088b7bd..690e7ba55 100644
--- a/frontend/src/hooks.server.ts
+++ b/frontend/src/hooks.server.ts
@@ -3,6 +3,7 @@ import type { User } from '$lib/utils/types';
import { redirect, type Handle, type RequestEvent, type HandleFetch } from '@sveltejs/kit';
import { setFlash } from 'sveltekit-flash-message/server';
import * as m from '$paraglide/messages';
+import { setLanguageTag } from '$paraglide/runtime';
async function ensureCsrfToken(event: RequestEvent): Promise {
let csrfToken = event.cookies.get('csrftoken') || '';
@@ -51,6 +52,7 @@ export const handle: Handle = async ({ event, resolve }) => {
const errorId = new URL(event.request.url).searchParams.get('error');
if (errorId) {
+ setLanguageTag(event.cookies.get('ciso_lang'));
setFlash({ type: 'error', message: m.failedSSO() }, event);
redirect(302, `/`);
}
From 822b4c7bcf6e93657c365940bdf68ad088172b9d Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 14 Jun 2024 12:16:07 +0200
Subject: [PATCH 031/115] Fix SAML urlpatterns
---
backend/iam/sso/saml/urls.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/iam/sso/saml/urls.py b/backend/iam/sso/saml/urls.py
index 2763a6dae..ce17b22b9 100644
--- a/backend/iam/sso/saml/urls.py
+++ b/backend/iam/sso/saml/urls.py
@@ -5,7 +5,7 @@
urlpatterns = [
re_path(
- r"^saml/(?P[^/]+)/",
+ r"^/(?P[^/]+)/",
include(
[
path(
From 85c220e51610549333dcbfad37ed6b65967d51fb Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 14 Jun 2024 13:50:09 +0200
Subject: [PATCH 032/115] Create settings app
---
backend/ciso_assistant/settings.py | 1 +
backend/settings/__init__.py | 0
backend/settings/apps.py | 6 ++++++
backend/settings/models.py | 26 ++++++++++++++++++++++++++
backend/settings/serializers.py | 23 +++++++++++++++++++++++
backend/settings/urls.py | 13 +++++++++++++
backend/settings/views.py | 29 +++++++++++++++++++++++++++++
7 files changed, 98 insertions(+)
create mode 100644 backend/settings/__init__.py
create mode 100644 backend/settings/apps.py
create mode 100644 backend/settings/models.py
create mode 100644 backend/settings/serializers.py
create mode 100644 backend/settings/urls.py
create mode 100644 backend/settings/views.py
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index 7b741891b..991a25680 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -124,6 +124,7 @@ def set_ciso_assistant_url(_, __, event_dict):
"tailwind",
"iam",
"core",
+ "settings",
"cal",
"django_filters",
# "debug_toolbar",
diff --git a/backend/settings/__init__.py b/backend/settings/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/settings/apps.py b/backend/settings/apps.py
new file mode 100644
index 000000000..71a36cb5a
--- /dev/null
+++ b/backend/settings/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SettingsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'settings'
diff --git a/backend/settings/models.py b/backend/settings/models.py
new file mode 100644
index 000000000..9aebfbdef
--- /dev/null
+++ b/backend/settings/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+
+class GlobalSettings(models.Model):
+ """
+ Global settings for the application.
+ New setting categories should only be added through data migrations.
+ """
+
+ class Names(models.TextChoices):
+ GENERAL = "general", "General"
+ SSO = "sso", "SSO"
+
+ # Name of the setting category.
+ name = models.CharField(
+ max_length=30,
+ unique=True,
+ primary_key=True,
+ choices=Names,
+ default=Names.GENERAL,
+ )
+ # Value of the setting.
+ value = models.JSONField()
+
+ def __str__(self):
+ return self.name
diff --git a/backend/settings/serializers.py b/backend/settings/serializers.py
new file mode 100644
index 000000000..1e6e8c547
--- /dev/null
+++ b/backend/settings/serializers.py
@@ -0,0 +1,23 @@
+from rest_framework import serializers
+
+from .models import GlobalSettings
+
+
+class GlobalSettingsSerializer(serializers.ModelSerializer):
+ def create(self, validated_data):
+ raise serializers.ValidationError(
+ "Global settings can only be created through data migrations."
+ )
+
+ def delete(self, instance):
+ raise serializers.ValidationError(
+ "Global settings can only be deleted through data migrations."
+ )
+
+ def update(self, instance, validated_data):
+ validated_data.pop("name")
+ return super().update(instance, validated_data)
+
+ class Meta:
+ model = GlobalSettings
+ fields = "__all__"
diff --git a/backend/settings/urls.py b/backend/settings/urls.py
new file mode 100644
index 000000000..64d7ed5df
--- /dev/null
+++ b/backend/settings/urls.py
@@ -0,0 +1,13 @@
+from django.urls import include, path
+from rest_framework import routers
+
+from .views import GlobalSettingsViewSet
+
+
+router = routers.DefaultRouter()
+router.register(r"", GlobalSettingsViewSet, basename="global-settings")
+
+
+urlpatterns = [
+ path("", include(router.urls)),
+]
diff --git a/backend/settings/views.py b/backend/settings/views.py
new file mode 100644
index 000000000..77723c01d
--- /dev/null
+++ b/backend/settings/views.py
@@ -0,0 +1,29 @@
+from rest_framework import viewsets
+from rest_framework.response import Response
+
+from .serializers import GlobalSettingsSerializer
+
+from .models import GlobalSettings
+
+
+class GlobalSettingsViewSet(viewsets.ModelViewSet):
+ queryset = GlobalSettings.objects.all()
+ serializer_class = GlobalSettingsSerializer
+
+ def create(self, request, *args, **kwargs):
+ return Response(
+ {"detail": "Global settings can only be created through data migrations."},
+ status=405,
+ )
+
+ def delete(self, request, *args, **kwargs):
+ return Response(
+ {"detail": "Global settings can only be deleted through data migrations."},
+ status=405,
+ )
+
+ def update(self, request, *args, **kwargs):
+ return Response(
+ {"detail": "Global settings can only be updated through data migrations."},
+ status=405,
+ )
From bc8fab9ad89307c5802fb733b7be490ecb663f8d Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 14 Jun 2024 13:50:32 +0200
Subject: [PATCH 033/115] Create SSOSettings proxy model
---
backend/iam/adapter.py | 12 +--
...dentityprovider.py => 0004_ssosettings.py} | 20 ++---
backend/iam/sso/models.py | 78 +++++++++++++++----
backend/iam/sso/serializers.py | 10 +--
backend/iam/sso/urls.py | 2 +-
backend/iam/sso/views.py | 6 +-
6 files changed, 86 insertions(+), 42 deletions(-)
rename backend/iam/migrations/{0004_identityprovider.py => 0004_ssosettings.py} (51%)
diff --git a/backend/iam/adapter.py b/backend/iam/adapter.py
index cbba8bdab..497b45268 100644
--- a/backend/iam/adapter.py
+++ b/backend/iam/adapter.py
@@ -37,19 +37,19 @@ def pre_social_login(self, request, sociallogin):
)
def list_apps(self, request, provider=None, client_id=None):
- """IdentityProvider's can be setup in the database, or, via
+ """SSOSettings's can be setup in the database, or, via
`settings.SOCIALACCOUNT_PROVIDERS`. This methods returns a uniform list
of all known apps matching the specified criteria, and blends both
(db/settings) sources of data.
"""
# NOTE: Avoid loading models at top due to registry boot...
- from .sso.models import IdentityProvider
+ from .sso.models import SSOSettings
# Map provider to the list of apps.
provider_to_apps = {}
# First, populate it with the DB backed apps.
- db_apps = IdentityProvider.objects.all()
+ db_apps = SSOSettings.objects.all()
if provider:
db_apps = db_apps.filter(Q(provider=provider) | Q(provider_id=provider))
if client_id:
@@ -69,7 +69,7 @@ def list_apps(self, request, provider=None, client_id=None):
apps = provider_to_apps.setdefault(p, [])
for config in app_configs:
- app = IdentityProvider(provider=p)
+ app = SSOSettings(provider=p)
for field in [
"name",
"provider_id",
@@ -100,7 +100,7 @@ def list_apps(self, request, provider=None, client_id=None):
return apps
def get_app(self, request, provider, client_id=None):
- from .sso.models import IdentityProvider
+ from .sso.models import SSOSettings
apps = self.list_apps(request, provider=provider, client_id=client_id)
if len(apps) > 1:
@@ -109,5 +109,5 @@ def get_app(self, request, provider, client_id=None):
raise MultipleObjectsReturned
apps = visible_apps
elif len(apps) == 0:
- raise IdentityProvider.DoesNotExist()
+ raise SSOSettings.DoesNotExist()
return apps[0]
diff --git a/backend/iam/migrations/0004_identityprovider.py b/backend/iam/migrations/0004_ssosettings.py
similarity index 51%
rename from backend/iam/migrations/0004_identityprovider.py
rename to backend/iam/migrations/0004_ssosettings.py
index be45cac3a..5e887a303 100644
--- a/backend/iam/migrations/0004_identityprovider.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -1,8 +1,5 @@
-# Generated by Django 5.0.4 on 2024-06-12 09:09
+# Generated by Django 5.0.4 on 2024-06-14 11:49
-import django.db.models.deletion
-import iam.models
-import uuid
from django.db import migrations, models
@@ -10,28 +7,25 @@ class Migration(migrations.Migration):
dependencies = [
('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
+ ('settings', '__first__'),
]
operations = [
migrations.CreateModel(
- name='IdentityProvider',
+ name='SSOSettings',
fields=[
- ('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')),
+ ('provider_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')),
],
options={
- 'verbose_name': 'identity provider',
- 'verbose_name_plural': 'identity providers',
+ 'managed': False,
+ 'proxy': True,
},
+ bases=('settings.globalsettings',),
),
]
diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py
index afb907f09..f29dfb6e8 100644
--- a/backend/iam/sso/models.py
+++ b/backend/iam/sso/models.py
@@ -1,29 +1,66 @@
-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 django.core.exceptions import ObjectDoesNotExist
+from django.db.models.query import QuerySet
-from iam.models import FolderMixin
+from allauth.socialaccount.models import providers
+from settings.models import GlobalSettings
-class IdentityProvider(AbstractBaseModel, FolderMixin):
- objects = SocialAppManager()
+class SSOSettingsQuerySet(QuerySet):
+ def __init__(self, model=None, query=None, using=None, hints=None):
+ super().__init__(model, query, using, hints)
+ self._result_cache = None
+ self._iter = None
+
+ def _fetch_all(self):
+ if self._result_cache is None:
+ try:
+ _settings = GlobalSettings.objects.get(name=GlobalSettings.Names.SSO)
+ self._result_cache = [
+ SSOSettings(
+ provider=_settings.value.get("provider"),
+ provider_id=_settings.value.get("provider_id"),
+ provider_name=_settings.value.get("provider_name"),
+ client_id=_settings.value.get("client_id"),
+ secret=_settings.value.get("secret"),
+ key=_settings.value.get("key"),
+ settings=_settings.value.get("settings"),
+ )
+ ]
+ except ObjectDoesNotExist:
+ self._result_cache = []
+
+ def iterator(self):
+ self._fetch_all()
+ for obj in self._result_cache:
+ yield obj
+
+ def get(self, *args, **kwargs):
+ self._fetch_all()
+ if not self._result_cache:
+ raise ObjectDoesNotExist("SSOSettings matching query does not exist.")
+ return self._result_cache[0]
+
+
+class SSOSettingsManager(models.Manager):
+ def get_queryset(self):
+ return SSOSettingsQuerySet(self.model, using=self._db)
+
+
+class SSOSettings(GlobalSettings):
+ objects = SSOSettingsManager()
- # 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)
+ provider_name = models.CharField(verbose_name=_("name"), max_length=200)
client_id = models.CharField(
verbose_name=_("client id"),
max_length=191,
@@ -41,11 +78,24 @@ class IdentityProvider(AbstractBaseModel, FolderMixin):
settings = models.JSONField(default=dict, blank=True)
class Meta:
- verbose_name = _("identity provider")
- verbose_name_plural = _("identity providers")
+ managed = False
+ proxy = True
def __str__(self):
- return self.name
+ return self.provider_name or "sso"
+
+ def save(self, *args, **kwargs):
+ _settings = self.global_settings
+ _settings.value = {
+ "provider": self.provider,
+ "provider_id": self.provider_id,
+ "provider_name": self.provider_name,
+ "client_id": self.client_id,
+ "secret": self.secret,
+ "key": self.key,
+ "settings": self.settings,
+ }
+ _settings.save()
def get_provider_display(self):
_providers = {p[0]: p[1] for p in providers.registry.as_choices()}
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index e08898239..ac615b41b 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -1,20 +1,20 @@
from allauth.socialaccount.providers.saml.provider import SAMLProvider
from rest_framework import serializers
-from .models import IdentityProvider
+from .models import SSOSettings
from core.serializers import BaseModelSerializer
-class IdentityProviderReadSerializer(BaseModelSerializer):
+class SSOSettingsReadSerializer(BaseModelSerializer):
provider = serializers.CharField(read_only=True, source="get_provider_display")
settings = serializers.CharField(read_only=True)
class Meta:
- model = IdentityProvider
+ model = SSOSettings
fields = "__all__"
-class IdentityProviderWriteSerializer(BaseModelSerializer):
+class SSOSettingsWriteSerializer(BaseModelSerializer):
attribute_mapping_uid = serializers.ListField(
child=serializers.CharField(
required=False, allow_blank=True, allow_null=True, write_only=True
@@ -86,7 +86,7 @@ class IdentityProviderWriteSerializer(BaseModelSerializer):
want_name_id_encrypted = serializers.BooleanField(required=False, write_only=True)
class Meta:
- model = IdentityProvider
+ model = SSOSettings
fields = "__all__"
def create(self, validated_data):
diff --git a/backend/iam/sso/urls.py b/backend/iam/sso/urls.py
index 3f4d09dc5..eb932d935 100644
--- a/backend/iam/sso/urls.py
+++ b/backend/iam/sso/urls.py
@@ -1,7 +1,7 @@
from django.urls import include, path
from rest_framework import routers
-from .views import IdentityProviderViewSet
+from .views import SSOSettingsViewSet
router = routers.DefaultRouter()
diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py
index 230706348..ee3213840 100644
--- a/backend/iam/sso/views.py
+++ b/backend/iam/sso/views.py
@@ -1,6 +1,6 @@
from rest_framework.response import Response
from core.views import BaseModelViewSet as AbstractBaseModelViewSet
-from .models import IdentityProvider
+from .models import SSOSettings
from rest_framework.decorators import action
from allauth.socialaccount import providers
@@ -9,8 +9,8 @@ class BaseModelViewSet(AbstractBaseModelViewSet):
serializers_module = "iam.sso.serializers"
-class IdentityProviderViewSet(BaseModelViewSet):
- model = IdentityProvider
+class SSOSettingsViewSet(BaseModelViewSet):
+ model = SSOSettings
@action(detail=False, name="Get provider choices")
def provider(self, request):
From 320d32b5f8ad873f243efc0e7e1ef9df0b1d6f40 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 14 Jun 2024 15:09:15 +0200
Subject: [PATCH 034/115] WIP
---
backend/core/apps.py | 9 +-
backend/core/permissions.py | 4 +-
backend/core/urls.py | 5 +-
backend/iam/migrations/0004_ssosettings.py | 4 +-
backend/settings/migrations/0001_initial.py | 25 ++++++
backend/settings/migrations/__init__.py | 0
backend/settings/models.py | 9 ++
frontend/messages/en.json | 3 +-
.../Forms/AutocompleteSelect.svelte | 1 -
.../src/lib/components/Forms/ModelForm.svelte | 9 +-
.../src/lib/components/SideBar/navData.ts | 16 ++--
.../src/routes/(app)/settings/+page.server.ts | 89 +++++++++++++++++++
.../src/routes/(app)/settings/+page.svelte | 13 +++
.../(authentication)/login/+page.svelte | 2 +-
14 files changed, 156 insertions(+), 33 deletions(-)
create mode 100644 backend/settings/migrations/0001_initial.py
create mode 100644 backend/settings/migrations/__init__.py
create mode 100644 frontend/src/routes/(app)/settings/+page.server.ts
create mode 100644 frontend/src/routes/(app)/settings/+page.svelte
diff --git a/backend/core/apps.py b/backend/core/apps.py
index df1063591..e25b6fb35 100644
--- a/backend/core/apps.py
+++ b/backend/core/apps.py
@@ -26,7 +26,6 @@
"view_loadedlibrary",
"view_storedlibrary",
"view_user",
- "view_identityprovider",
]
APPROVER_PERMISSIONS_LIST = [
@@ -51,7 +50,6 @@
"view_storedlibrary",
"view_loadedlibrary",
"view_user",
- "view_identityprovider",
]
ANALYST_PERMISSIONS_LIST = [
@@ -106,7 +104,6 @@
"view_storedlibrary",
"view_loadedlibrary",
"view_user",
- "view_identityprovider",
]
DOMAIN_MANAGER_PERMISSIONS_LIST = [
@@ -166,7 +163,6 @@
"view_storedlibrary",
"view_loadedlibrary",
"view_user",
- "view_identityprovider",
]
ADMINISTRATOR_PERMISSIONS_LIST = [
@@ -249,10 +245,7 @@
"delete_loadedlibrary",
"backup",
"restore",
- "view_identityprovider",
- "add_identityprovider",
- "change_identityprovider",
- "delete_identityprovider",
+ "change_globalsettings",
]
diff --git a/backend/core/permissions.py b/backend/core/permissions.py
index 5d1a6acda..0ffed5017 100644
--- a/backend/core/permissions.py
+++ b/backend/core/permissions.py
@@ -33,7 +33,9 @@ def has_object_permission(self, request: Request, view, obj):
if not perms:
return False
_codename = perms[0].split(".")[1]
- if request.method in ["GET", "OPTIONS", "HEAD"] and obj.is_published:
+ if request.method in ["GET", "OPTIONS", "HEAD"] and getattr(
+ obj, "is_published", False
+ ):
return True
perm = Permission.objects.get(codename=_codename)
# special case of risk acceptance approval
diff --git a/backend/core/urls.py b/backend/core/urls.py
index 92d32c25b..adf89dfad 100644
--- a/backend/core/urls.py
+++ b/backend/core/urls.py
@@ -1,4 +1,4 @@
-from iam.sso.views import IdentityProviderViewSet
+from iam.sso.views import SSOSettingsViewSet
from .views import *
from library.views import StoredLibraryViewSet, LoadedLibraryViewSet
from iam.sso.saml.views import FinishACSView
@@ -44,7 +44,7 @@
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"
+ r"identity-providers", SSOSettingsViewSet, basename="identity-providers"
)
@@ -52,6 +52,7 @@
path("", include(router.urls)),
path("iam/", include("iam.urls")),
path("serdes/", include("serdes.urls")),
+ path("settings/", include("settings.urls")),
path("csrf/", get_csrf_token, name="get_csrf_token"),
path("build/", get_build, name="get_build"),
path("license/", license, name="license"),
diff --git a/backend/iam/migrations/0004_ssosettings.py b/backend/iam/migrations/0004_ssosettings.py
index 5e887a303..f531f2c0c 100644
--- a/backend/iam/migrations/0004_ssosettings.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.4 on 2024-06-14 11:49
+# Generated by Django 5.0.4 on 2024-06-14 12:42
from django.db import migrations, models
@@ -7,7 +7,7 @@ class Migration(migrations.Migration):
dependencies = [
('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
- ('settings', '__first__'),
+ ('settings', '0001_initial'),
]
operations = [
diff --git a/backend/settings/migrations/0001_initial.py b/backend/settings/migrations/0001_initial.py
new file mode 100644
index 000000000..75b439e0e
--- /dev/null
+++ b/backend/settings/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.0.4 on 2024-06-14 12:42
+
+import django.db.models.deletion
+import iam.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='GlobalSettings',
+ fields=[
+ ('name', models.CharField(choices=[('general', 'General'), ('sso', 'SSO')], default='general', max_length=30, primary_key=True, serialize=False, unique=True)),
+ ('value', models.JSONField()),
+ ('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')),
+ ],
+ ),
+ ]
diff --git a/backend/settings/migrations/__init__.py b/backend/settings/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/settings/models.py b/backend/settings/models.py
index 9aebfbdef..127c328a9 100644
--- a/backend/settings/models.py
+++ b/backend/settings/models.py
@@ -1,5 +1,7 @@
from django.db import models
+from iam.models import Folder, FolderMixin
+
class GlobalSettings(models.Model):
"""
@@ -22,5 +24,12 @@ class Names(models.TextChoices):
# Value of the setting.
value = models.JSONField()
+ folder = models.ForeignKey(
+ Folder,
+ on_delete=models.CASCADE,
+ related_name="%(class)s_folder",
+ default=Folder.get_root_folder,
+ )
+
def __str__(self):
return self.name
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 59b8b9e34..0e003bb97 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -569,5 +569,6 @@
"settings": "Settings",
"identityProvider": "Identity provider",
"identityProviders": "Identity providers",
- "addIdentityProvider": "Add identity provider"
+ "addIdentityProvider": "Add identity provider",
+ "globalSettings": "Global settings"
}
diff --git a/frontend/src/lib/components/Forms/AutocompleteSelect.svelte b/frontend/src/lib/components/Forms/AutocompleteSelect.svelte
index f57f0107c..d9a243895 100644
--- a/frontend/src/lib/components/Forms/AutocompleteSelect.svelte
+++ b/frontend/src/lib/components/Forms/AutocompleteSelect.svelte
@@ -1,7 +1,6 @@
+
+
diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte
index b1a861b6d..d2353317a 100644
--- a/frontend/src/routes/(authentication)/login/+page.svelte
+++ b/frontend/src/routes/(authentication)/login/+page.svelte
@@ -81,7 +81,7 @@
Date: Tue, 18 Jun 2024 14:37:50 +0200
Subject: [PATCH 035/115] Implement proper API routing for settings
---
backend/iam/sso/models.py | 7 ++++++-
backend/iam/sso/serializers.py | 1 +
backend/iam/sso/views.py | 13 +++++++++++++
backend/settings/routers.py | 27 +++++++++++++++++++++++++++
backend/settings/urls.py | 15 +++++++++++++--
5 files changed, 60 insertions(+), 3 deletions(-)
create mode 100644 backend/settings/routers.py
diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py
index f29dfb6e8..af8bd9c03 100644
--- a/backend/iam/sso/models.py
+++ b/backend/iam/sso/models.py
@@ -19,6 +19,8 @@ def _fetch_all(self):
_settings = GlobalSettings.objects.get(name=GlobalSettings.Names.SSO)
self._result_cache = [
SSOSettings(
+ id=_settings.id,
+ name=_settings.name,
provider=_settings.value.get("provider"),
provider_id=_settings.value.get("provider_id"),
provider_name=_settings.value.get("provider_name"),
@@ -81,8 +83,11 @@ class Meta:
managed = False
proxy = True
+ def get_name(self):
+ return GlobalSettings.Names.SSO.label
+
def __str__(self):
- return self.provider_name or "sso"
+ return self.get_name()
def save(self, *args, **kwargs):
_settings = self.global_settings
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index ac615b41b..8fc079a38 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -6,6 +6,7 @@
class SSOSettingsReadSerializer(BaseModelSerializer):
+ name = serializers.CharField(read_only=True, source="get_name")
provider = serializers.CharField(read_only=True, source="get_provider_display")
settings = serializers.CharField(read_only=True)
diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py
index ee3213840..6df2b8231 100644
--- a/backend/iam/sso/views.py
+++ b/backend/iam/sso/views.py
@@ -1,6 +1,7 @@
from rest_framework.response import Response
from core.views import BaseModelViewSet as AbstractBaseModelViewSet
from .models import SSOSettings
+from .serializers import SSOSettingsReadSerializer
from rest_framework.decorators import action
from allauth.socialaccount import providers
@@ -12,6 +13,18 @@ class BaseModelViewSet(AbstractBaseModelViewSet):
class SSOSettingsViewSet(BaseModelViewSet):
model = SSOSettings
+ def retrieve(self, request, *args, **kwargs):
+ instance = self.model.objects.get()
+ serializer = self.get_serializer(instance)
+ return Response(serializer.data)
+
+ def update(self, request, *args, **kwargs):
+ instance = self.model.objects.get()
+ serializer = self.get_serializer(instance, data=request.data)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return Response(serializer.data)
+
@action(detail=False, name="Get provider choices")
def provider(self, request):
_providers = providers.registry.as_choices()
diff --git a/backend/settings/routers.py b/backend/settings/routers.py
new file mode 100644
index 000000000..d2e2be69d
--- /dev/null
+++ b/backend/settings/routers.py
@@ -0,0 +1,27 @@
+from rest_framework.routers import Route, DynamicRoute, SimpleRouter
+
+
+class DefaultSettingsRouter(SimpleRouter):
+ """
+ A custom router for settings views.
+ """
+
+ routes = [
+ Route(
+ url=r"^{prefix}{trailing_slash}$",
+ mapping={
+ "get": "retrieve",
+ "put": "update",
+ "patch": "partial_update",
+ },
+ name="{basename}-detail",
+ detail=True,
+ initkwargs={"suffix": "Instance"},
+ ),
+ DynamicRoute(
+ url=r"^{prefix}/{url_path}{trailing_slash}$",
+ name="{basename}-{url_name}",
+ detail=True,
+ initkwargs={},
+ ),
+ ]
diff --git a/backend/settings/urls.py b/backend/settings/urls.py
index 64d7ed5df..7659d032b 100644
--- a/backend/settings/urls.py
+++ b/backend/settings/urls.py
@@ -1,13 +1,24 @@
from django.urls import include, path
from rest_framework import routers
+from iam.sso.views import SSOSettingsViewSet
+
from .views import GlobalSettingsViewSet
+from .routers import DefaultSettingsRouter
router = routers.DefaultRouter()
-router.register(r"", GlobalSettingsViewSet, basename="global-settings")
+router.register(r"global", GlobalSettingsViewSet, basename="global-settings")
+
+settings_router = DefaultSettingsRouter()
+settings_router.register(
+ r"sso",
+ SSOSettingsViewSet,
+ basename="sso-settings",
+)
urlpatterns = [
- path("", include(router.urls)),
+ path(r"", include(router.urls)),
+ path(r"", include(settings_router.urls)),
]
From c1dd0a66f00f4134c6391383e1825419569b0ada Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 14:38:05 +0200
Subject: [PATCH 036/115] Refactor SSOSettings model
---
backend/iam/migrations/0004_ssosettings.py | 2 +-
backend/settings/migrations/0001_initial.py | 12 ++++++++++--
backend/settings/models.py | 13 +++----------
3 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/backend/iam/migrations/0004_ssosettings.py b/backend/iam/migrations/0004_ssosettings.py
index f531f2c0c..61af0043f 100644
--- a/backend/iam/migrations/0004_ssosettings.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.4 on 2024-06-14 12:42
+# Generated by Django 5.0.4 on 2024-06-18 10:47
from django.db import migrations, models
diff --git a/backend/settings/migrations/0001_initial.py b/backend/settings/migrations/0001_initial.py
index 75b439e0e..0dffc5f0e 100644
--- a/backend/settings/migrations/0001_initial.py
+++ b/backend/settings/migrations/0001_initial.py
@@ -1,7 +1,8 @@
-# Generated by Django 5.0.4 on 2024-06-14 12:42
+# Generated by Django 5.0.4 on 2024-06-18 10:47
import django.db.models.deletion
import iam.models
+import uuid
from django.db import migrations, models
@@ -17,9 +18,16 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='GlobalSettings',
fields=[
- ('name', models.CharField(choices=[('general', 'General'), ('sso', 'SSO')], default='general', max_length=30, primary_key=True, serialize=False, unique=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')),
+ ('name', models.CharField(choices=[('general', 'General'), ('sso', 'SSO')], default='general', max_length=30, unique=True)),
('value', models.JSONField()),
('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')),
],
+ options={
+ 'abstract': False,
+ },
),
]
diff --git a/backend/settings/models.py b/backend/settings/models.py
index 127c328a9..500211f0e 100644
--- a/backend/settings/models.py
+++ b/backend/settings/models.py
@@ -1,9 +1,10 @@
from django.db import models
-from iam.models import Folder, FolderMixin
+from iam.models import FolderMixin
+from core.base_models import AbstractBaseModel
-class GlobalSettings(models.Model):
+class GlobalSettings(AbstractBaseModel, FolderMixin):
"""
Global settings for the application.
New setting categories should only be added through data migrations.
@@ -17,19 +18,11 @@ class Names(models.TextChoices):
name = models.CharField(
max_length=30,
unique=True,
- primary_key=True,
choices=Names,
default=Names.GENERAL,
)
# Value of the setting.
value = models.JSONField()
- folder = models.ForeignKey(
- Folder,
- on_delete=models.CASCADE,
- related_name="%(class)s_folder",
- default=Folder.get_root_folder,
- )
-
def __str__(self):
return self.name
From bdeec8d0c25ffe752334d482e1f28b4fc2d36a20 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 14:38:21 +0200
Subject: [PATCH 037/115] Add view_globalsettings permission to Administrator
role
---
backend/core/apps.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/backend/core/apps.py b/backend/core/apps.py
index e25b6fb35..cff098355 100644
--- a/backend/core/apps.py
+++ b/backend/core/apps.py
@@ -245,6 +245,7 @@
"delete_loadedlibrary",
"backup",
"restore",
+ "view_globalsettings",
"change_globalsettings",
]
From 0718cd1e3e7a0c209643721bdb5994214f4335a4 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:00:03 +0200
Subject: [PATCH 038/115] Fix urlpatterns
---
backend/core/urls.py | 4 +++-
backend/iam/sso/saml/urls.py | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/backend/core/urls.py b/backend/core/urls.py
index adf89dfad..3c89c2581 100644
--- a/backend/core/urls.py
+++ b/backend/core/urls.py
@@ -61,9 +61,11 @@
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/saml/", include("iam.sso.saml.urls")
+ ), # NOTE: This has to be placed before the allauth urls, otherwise our ACS implementation will not be used
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/sso/saml/urls.py b/backend/iam/sso/saml/urls.py
index ce17b22b9..60cc53396 100644
--- a/backend/iam/sso/saml/urls.py
+++ b/backend/iam/sso/saml/urls.py
@@ -5,7 +5,7 @@
urlpatterns = [
re_path(
- r"^/(?P[^/]+)/",
+ r"^(?P[^/]+)/",
include(
[
path(
From 01e5b941a84917072dc0d035a428eba0814ef997 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:01:15 +0200
Subject: [PATCH 039/115] Fix settings serialization
---
backend/iam/sso/models.py | 19 ++--
backend/iam/sso/serializers.py | 174 +++++++++++++++++++++++++--------
backend/iam/sso/views.py | 11 ++-
3 files changed, 147 insertions(+), 57 deletions(-)
diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py
index af8bd9c03..0f42958ae 100644
--- a/backend/iam/sso/models.py
+++ b/backend/iam/sso/models.py
@@ -21,10 +21,13 @@ def _fetch_all(self):
SSOSettings(
id=_settings.id,
name=_settings.name,
+ created_at=_settings.created_at,
+ updated_at=_settings.updated_at,
+ is_published=_settings.is_published,
provider=_settings.value.get("provider"),
- provider_id=_settings.value.get("provider_id"),
- provider_name=_settings.value.get("provider_name"),
client_id=_settings.value.get("client_id"),
+ provider_id=_settings.value.get("provider_id"),
+ provider_name=_settings.value.get("name"),
secret=_settings.value.get("secret"),
key=_settings.value.get("key"),
settings=_settings.value.get("settings"),
@@ -90,17 +93,7 @@ def __str__(self):
return self.get_name()
def save(self, *args, **kwargs):
- _settings = self.global_settings
- _settings.value = {
- "provider": self.provider,
- "provider_id": self.provider_id,
- "provider_name": self.provider_name,
- "client_id": self.client_id,
- "secret": self.secret,
- "key": self.key,
- "settings": self.settings,
- }
- _settings.save()
+ raise NotImplementedError("SSOSettings is read-only.")
def get_provider_display(self):
_providers = {p[0]: p[1] for p in providers.registry.as_choices()}
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index 8fc079a38..c913d8f97 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -1,5 +1,7 @@
from allauth.socialaccount.providers.saml.provider import SAMLProvider
from rest_framework import serializers
+
+from settings.models import GlobalSettings
from .models import SSOSettings
from core.serializers import BaseModelSerializer
@@ -12,95 +14,183 @@ class SSOSettingsReadSerializer(BaseModelSerializer):
class Meta:
model = SSOSettings
- fields = "__all__"
+ exclude = ["value"]
class SSOSettingsWriteSerializer(BaseModelSerializer):
+ provider = serializers.CharField(
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ )
+ provider_id = serializers.CharField(
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ )
+ client_id = serializers.CharField(
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ )
+ provider_name = serializers.CharField(
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.name",
+ )
attribute_mapping_uid = serializers.ListField(
child=serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
),
- write_only=True,
required=False,
allow_null=True,
+ source="settings.attribute_mapping.uid",
)
attribute_mapping_email_verified = serializers.ListField(
child=serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
),
- write_only=True,
required=False,
allow_null=True,
+ source="settings.attribute_mapping.email_verified",
)
attribute_mapping_email = serializers.ListField(
child=serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
),
- write_only=True,
required=False,
allow_null=True,
+ source="settings.attribute_mapping.email",
)
idp_entity_id = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.idp.entity_id",
)
metadata_url = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.idp.metadata_url",
)
sso_url = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.idp.sso_url",
)
slo_url = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.idp.slo_url",
)
x509cert = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.idp.x509cert",
)
sp_entity_id = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.sp.entity_id",
)
allow_repeat_attribute_name = serializers.BooleanField(
- required=False, write_only=True
+ required=False,
+ source="settings.advanced.allow_repeat_attribute_name",
)
allow_single_label_domains = serializers.BooleanField(
- required=False, write_only=True
+ required=False,
+ source="settings.advanced.allow_single_label_domains",
+ )
+ authn_request_signed = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.authn_request_signed",
)
- authn_request_signed = serializers.BooleanField(required=False, write_only=True)
digest_algorithm = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.advanced.digest_algorithm",
+ )
+ logout_request_signed = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.logout_request_signed",
+ )
+ logout_response_signed = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.logout_response_signed",
+ )
+ metadata_signed = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.metadata_signed",
+ )
+ name_id_encrypted = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.name_id_encrypted",
)
- logout_request_signed = serializers.BooleanField(required=False, write_only=True)
- logout_response_signed = serializers.BooleanField(required=False, write_only=True)
- metadata_signed = serializers.BooleanField(required=False, write_only=True)
- name_id_encrypted = serializers.BooleanField(required=False, write_only=True)
reject_deprecated_algorithm = serializers.BooleanField(
- required=False, write_only=True
+ required=False,
+ source="settings.advanced.reject_deprecated_algorithm",
+ )
+ reject_idp_initiated_sso = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.reject_idp_initiated_sso",
)
- reject_idp_initiated_sso = serializers.BooleanField(required=False, write_only=True)
signature_algorithm = serializers.CharField(
- required=False, allow_blank=True, allow_null=True, write_only=True
- )
- want_assertion_encrypted = serializers.BooleanField(required=False, write_only=True)
- want_assertion_signed = serializers.BooleanField(required=False, write_only=True)
- want_attribute_statement = serializers.BooleanField(required=False, write_only=True)
- want_message_signed = serializers.BooleanField(required=False, write_only=True)
- want_name_id = serializers.BooleanField(required=False, write_only=True)
- want_name_id_encrypted = serializers.BooleanField(required=False, write_only=True)
+ required=False,
+ allow_blank=True,
+ allow_null=True,
+ source="settings.advanced.signature_algorithm",
+ )
+ want_assertion_encrypted = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.want_assertion_encrypted",
+ )
+ want_assertion_signed = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.want_assertion_signed",
+ )
+ want_attribute_statement = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.want_attribute_statement",
+ )
+ want_message_signed = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.want_message_signed",
+ )
+ want_name_id = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.want_name_id",
+ )
+ want_name_id_encrypted = serializers.BooleanField(
+ required=False,
+ source="settings.advanced.want_name_id_encrypted",
+ )
class Meta:
model = SSOSettings
- fields = "__all__"
-
- def create(self, validated_data):
- if validated_data.get("provider") == "saml":
- settings = self.build_saml_settings(validated_data)
- validated_data["settings"] = settings
- return super().create(validated_data)
+ exclude = ["value"]
def update(self, instance, validated_data):
- if validated_data.get("provider") == "saml":
- settings = self.build_saml_settings(validated_data)
- validated_data["settings"] = settings
- return super().update(instance, validated_data)
+ # if validated_data.get("provider") == "saml":
+ # settings = self.build_saml_settings(validated_data)
+ # validated_data["settings"] = settings
+ settings_object = GlobalSettings.objects.get(name=GlobalSettings.Names.SSO)
+ settings_object.value = validated_data
+ # print(settings_object.value)
+ settings_object.save()
+ return instance
def build_saml_settings(self, validated_data):
default_attribute_mapping = SAMLProvider.default_attribute_mapping
diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py
index 6df2b8231..e944f7d12 100644
--- a/backend/iam/sso/views.py
+++ b/backend/iam/sso/views.py
@@ -1,7 +1,7 @@
from rest_framework.response import Response
from core.views import BaseModelViewSet as AbstractBaseModelViewSet
from .models import SSOSettings
-from .serializers import SSOSettingsReadSerializer
+from .serializers import SSOSettingsReadSerializer, SSOSettingsWriteSerializer
from rest_framework.decorators import action
from allauth.socialaccount import providers
@@ -25,7 +25,14 @@ def update(self, request, *args, **kwargs):
serializer.save()
return Response(serializer.data)
- @action(detail=False, name="Get provider choices")
+ @action(detail=True, name="Get provider choices")
def provider(self, request):
_providers = providers.registry.as_choices()
return Response({p[0]: p[1] for p in _providers})
+
+ def get_object(self):
+ return SSOSettings.objects.get()
+
+ @action(detail=True, name="Get write data")
+ def object(self, request, pk=None):
+ return Response(SSOSettingsWriteSerializer(self.get_object()).data)
From 3a0fe23a25f3ea2f9b8db90d631d20de1c478dbe Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:01:28 +0200
Subject: [PATCH 040/115] Fix settings ModelForm and schema
---
frontend/src/lib/components/Forms/ModelForm.svelte | 8 +++++++-
frontend/src/lib/utils/schemas.ts | 6 +++---
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 16c11b717..95c60cd0a 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -426,7 +426,13 @@
{/if}
{:else if URLModel === 'identity-providers'}
-
+
+
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index cc0163fc6..1abdf9d43 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -214,7 +214,7 @@ export const EvidenceSchema = baseNamedObject({
export const IdentityProviderSchema = z.object({
provider: z.string(),
provider_id: z.string().optional(),
- name: z.string(),
+ provider_name: z.string(),
client_id: z.string(),
secret: z.string().optional(),
key: z.string().optional(),
@@ -231,8 +231,8 @@ export const IdentityProviderSchema = z.object({
.optional(),
idp_entity_id: z.string().optional(),
metadata_url: z.string().url().optional(),
- sso_url: z.string().url().optional(),
- slo_url: z.string().url().optional(),
+ sso_url: z.string().optional().nullable(),
+ slo_url: z.string().optional().nullable(),
x509cert: z.string().optional(),
sp_entity_id: z.string().optional(),
allow_repeat_attribute_name: z.boolean().default(true),
From ec4379281f494e01c9237e86e1423f30c8f33cb4 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:01:48 +0200
Subject: [PATCH 041/115] Supply default value for GlobalSettings.value
---
.../0002_alter_globalsettings_value.py | 18 ++++++++++++++++++
backend/settings/models.py | 2 +-
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 backend/settings/migrations/0002_alter_globalsettings_value.py
diff --git a/backend/settings/migrations/0002_alter_globalsettings_value.py b/backend/settings/migrations/0002_alter_globalsettings_value.py
new file mode 100644
index 000000000..d40d663e3
--- /dev/null
+++ b/backend/settings/migrations/0002_alter_globalsettings_value.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-06-18 12:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('settings', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='globalsettings',
+ name='value',
+ field=models.JSONField(default=dict),
+ ),
+ ]
diff --git a/backend/settings/models.py b/backend/settings/models.py
index 500211f0e..5d9e8c323 100644
--- a/backend/settings/models.py
+++ b/backend/settings/models.py
@@ -22,7 +22,7 @@ class Names(models.TextChoices):
default=Names.GENERAL,
)
# Value of the setting.
- value = models.JSONField()
+ value = models.JSONField(default=dict)
def __str__(self):
return self.name
From d51d4851718cb520e30d8c2791fafb10b2f43d20 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:02:30 +0200
Subject: [PATCH 042/115] Fix SSO Settings form
---
.../src/routes/(app)/settings/+page.server.ts | 18 ++++++++----------
.../src/routes/(app)/settings/+page.svelte | 7 +------
2 files changed, 9 insertions(+), 16 deletions(-)
diff --git a/frontend/src/routes/(app)/settings/+page.server.ts b/frontend/src/routes/(app)/settings/+page.server.ts
index b5b7e1f5e..bbedc7aad 100644
--- a/frontend/src/routes/(app)/settings/+page.server.ts
+++ b/frontend/src/routes/(app)/settings/+page.server.ts
@@ -18,17 +18,15 @@ import { z } from 'zod';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, fetch }) => {
- const settings = await fetch(`${BASE_API_URL}/settings/`)
- .then((res) => res.json())
- .then((data) => data.results);
+ const settings = await fetch(`${BASE_API_URL}/settings/sso/object/`).then((res) => res.json());
const selectOptions: Record = {};
- const settingsModel = getModelInfo('identity-providers');
+ const model = getModelInfo('identity-providers');
- if (settingsModel.selectFields) {
- for (const selectField of settingsModel.selectFields) {
- const url = `${BASE_API_URL}/risk-settingss/${selectField.field}/`;
+ if (model.selectFields) {
+ for (const selectField of model.selectFields) {
+ const url = `${BASE_API_URL}/settings/sso/${selectField.field}/`;
const response = await fetch(url);
if (response.ok) {
selectOptions[selectField.field] = await response.json().then((data) =>
@@ -43,10 +41,10 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
}
}
- settingsModel.selectOptions = selectOptions;
+ model.selectOptions = selectOptions;
- const form = await superValidate(zod(IdentityProviderSchema));
- return { settings, form, settingsModel };
+ const form = await superValidate(settings, zod(IdentityProviderSchema), { errors: false });
+ return { settings, form, model };
};
export const actions: Actions = {
diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte
index e31547c01..04a723ebc 100644
--- a/frontend/src/routes/(app)/settings/+page.svelte
+++ b/frontend/src/routes/(app)/settings/+page.svelte
@@ -1,13 +1,8 @@
-
+
From bb9aa81de726610c34478a9545858bc5d1e225b7 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:12:34 +0200
Subject: [PATCH 043/115] Make SSOSettings a regular unmanaged model instead of
an unmanaged proxy model
---
backend/iam/sso/models.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py
index 0f42958ae..988b576c9 100644
--- a/backend/iam/sso/models.py
+++ b/backend/iam/sso/models.py
@@ -84,7 +84,6 @@ class SSOSettings(GlobalSettings):
class Meta:
managed = False
- proxy = True
def get_name(self):
return GlobalSettings.Names.SSO.label
From 98971b13780da6d03ca93b48c7bd34821713a50f Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:12:46 +0200
Subject: [PATCH 044/115] Add django-allauth and lxml dependencies
---
backend/requirements.txt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 3b690d37a..1f8c1c25f 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -20,3 +20,5 @@ drf-spectacular==0.27.2
django-rest-knox==4.2.0
django-allauth[socialaccount]>=0.63.3
pre-commit==3.7.1
+django-allauth==0.63.3
+lxml==5.2.2
From 22e16ff981609516fb5b0b46d27e438bd24b3041 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:13:55 +0200
Subject: [PATCH 045/115] chore: Run ruff formatter
---
backend/iam/migrations/0004_ssosettings.py | 55 ++++++++++++++-----
backend/settings/apps.py | 4 +-
backend/settings/migrations/0001_initial.py | 54 ++++++++++++++----
.../0002_alter_globalsettings_value.py | 7 +--
4 files changed, 89 insertions(+), 31 deletions(-)
diff --git a/backend/iam/migrations/0004_ssosettings.py b/backend/iam/migrations/0004_ssosettings.py
index 61af0043f..b227bedb9 100644
--- a/backend/iam/migrations/0004_ssosettings.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -4,28 +4,55 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
- ('settings', '0001_initial'),
+ ("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"),
+ ("settings", "0001_initial"),
]
operations = [
migrations.CreateModel(
- name='SSOSettings',
+ name="SSOSettings",
fields=[
- ('provider', models.CharField(max_length=30, verbose_name='provider')),
- ('provider_id', models.CharField(blank=True, max_length=200, verbose_name='provider ID')),
- ('provider_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)),
+ ("provider", models.CharField(max_length=30, verbose_name="provider")),
+ (
+ "provider_id",
+ models.CharField(
+ blank=True, max_length=200, verbose_name="provider ID"
+ ),
+ ),
+ (
+ "provider_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)),
],
options={
- 'managed': False,
- 'proxy': True,
+ "managed": False,
+ "proxy": True,
},
- bases=('settings.globalsettings',),
+ bases=("settings.globalsettings",),
),
]
diff --git a/backend/settings/apps.py b/backend/settings/apps.py
index 71a36cb5a..727634f96 100644
--- a/backend/settings/apps.py
+++ b/backend/settings/apps.py
@@ -2,5 +2,5 @@
class SettingsConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'settings'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "settings"
diff --git a/backend/settings/migrations/0001_initial.py b/backend/settings/migrations/0001_initial.py
index 0dffc5f0e..182ab6dee 100644
--- a/backend/settings/migrations/0001_initial.py
+++ b/backend/settings/migrations/0001_initial.py
@@ -7,27 +7,59 @@
class Migration(migrations.Migration):
-
initial = True
dependencies = [
- ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
+ ("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"),
]
operations = [
migrations.CreateModel(
- name='GlobalSettings',
+ name="GlobalSettings",
fields=[
- ('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')),
- ('name', models.CharField(choices=[('general', 'General'), ('sso', 'SSO')], default='general', max_length=30, unique=True)),
- ('value', models.JSONField()),
- ('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')),
+ (
+ "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"),
+ ),
+ (
+ "name",
+ models.CharField(
+ choices=[("general", "General"), ("sso", "SSO")],
+ default="general",
+ max_length=30,
+ unique=True,
+ ),
+ ),
+ ("value", models.JSONField()),
+ (
+ "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",
+ ),
+ ),
],
options={
- 'abstract': False,
+ "abstract": False,
},
),
]
diff --git a/backend/settings/migrations/0002_alter_globalsettings_value.py b/backend/settings/migrations/0002_alter_globalsettings_value.py
index d40d663e3..4df3cd05f 100644
--- a/backend/settings/migrations/0002_alter_globalsettings_value.py
+++ b/backend/settings/migrations/0002_alter_globalsettings_value.py
@@ -4,15 +4,14 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('settings', '0001_initial'),
+ ("settings", "0001_initial"),
]
operations = [
migrations.AlterField(
- model_name='globalsettings',
- name='value',
+ model_name="globalsettings",
+ name="value",
field=models.JSONField(default=dict),
),
]
From 554ee4c24f072cb5ab016cd43053e01176201228 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:14:34 +0200
Subject: [PATCH 046/115] Remove test_saml.py
---
backend/test_saml.py | 147 -------------------------------------------
1 file changed, 147 deletions(-)
delete mode 100644 backend/test_saml.py
diff --git a/backend/test_saml.py b/backend/test_saml.py
deleted file mode 100644
index 6cfef1673..000000000
--- a/backend/test_saml.py
+++ /dev/null
@@ -1,147 +0,0 @@
-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": "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).
- // 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))
From af015aa92ffe6468d2190698fc93e2e7c73060e5 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:17:43 +0200
Subject: [PATCH 047/115] Add django-allauth[saml] dependency
---
backend/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 1f8c1c25f..93c3cca57 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -20,5 +20,5 @@ drf-spectacular==0.27.2
django-rest-knox==4.2.0
django-allauth[socialaccount]>=0.63.3
pre-commit==3.7.1
+django-allauth[saml]==0.63.3
django-allauth==0.63.3
-lxml==5.2.2
From 75f603d0a175de7fb74b3321f5f1171e09c9d41a Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Tue, 18 Jun 2024 18:19:07 +0200
Subject: [PATCH 048/115] chore: Run prettier
---
frontend/src/lib/allauth.js | 165 ++++++++++++++++++------------------
frontend/src/lib/django.js | 32 +++----
2 files changed, 98 insertions(+), 99 deletions(-)
diff --git a/frontend/src/lib/allauth.js b/frontend/src/lib/allauth.js
index 6ecc0bab7..df7ca8df1 100644
--- a/frontend/src/lib/allauth.js
+++ b/frontend/src/lib/allauth.js
@@ -1,100 +1,99 @@
-import { getCSRFToken } from './django.js'
+import { getCSRFToken } from './django.js';
import { BASE_API_URL } from '$lib/utils/constants';
const Client = Object.freeze({
- APP: 'app',
- BROWSER: 'browser'
-})
+ APP: 'app',
+ BROWSER: 'browser'
+});
-const CLIENT = Client.BROWSER
+const CLIENT = Client.BROWSER;
-const BASE_URL = `${BASE_API_URL}/_allauth/${CLIENT}/v1`
+const BASE_URL = `${BASE_API_URL}/_allauth/${CLIENT}/v1`;
const ACCEPT_JSON = {
- accept: 'application/json'
-}
+ accept: 'application/json'
+};
export const AuthProcess = Object.freeze({
- LOGIN: 'login',
- CONNECT: 'connect'
-})
+ 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'
-})
+ 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'
-})
+ // 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()
+ 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()
- })
+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
index ba9e6117b..c70169751 100644
--- a/frontend/src/lib/django.js
+++ b/frontend/src/lib/django.js
@@ -1,18 +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
+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')
+export function getCSRFToken() {
+ return getCookie('csrftoken');
}
From 5cdc3dbce55dceb29f70ac43d2d19a3283f0cd29 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 11:32:22 +0200
Subject: [PATCH 049/115] Initialize STATIC_URL outside of debug
---
backend/ciso_assistant/settings.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index 991a25680..5dcf07d92 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -207,6 +207,9 @@ def set_ciso_assistant_url(_, __, event_dict):
"MIN_REFRESH_INTERVAL": 60,
}
+# Empty outside of debug mode so that allauth middleware does not raise an error
+STATIC_URL = ""
+
if DEBUG:
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
"rest_framework.renderers.BrowsableAPIRenderer"
From 4b3f7d1916e54716565e3ea5ff3774adb62a9b74 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 11:32:45 +0200
Subject: [PATCH 050/115] Throw a 404 if SSO is not set up
---
backend/iam/sso/saml/views.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/backend/iam/sso/saml/views.py b/backend/iam/sso/saml/views.py
index 634a82feb..0de3e00cc 100644
--- a/backend/iam/sso/saml/views.py
+++ b/backend/iam/sso/saml/views.py
@@ -14,6 +14,7 @@
render_authentication_error,
)
from django.http import HttpRequest, HttpResponseRedirect
+from django.http.response import Http404
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
@@ -22,6 +23,7 @@
import structlog
from iam.models import User
+from iam.sso.models import SSOSettings
from iam.utils import generate_token
logger = structlog.get_logger(__name__)
@@ -43,6 +45,8 @@ def dispatch(self, request, organization_slug):
class FinishACSView(SAMLViewMixin, View):
def dispatch(self, request, organization_slug):
+ if len(SSOSettings.objects.all()) == 0:
+ raise Http404()
provider = self.get_provider(organization_slug)
acs_session = LoginSession(request, "saml_acs_session", "saml-acs-session")
acs_request = None
From c2eacc7485c01f75085c62b53730accedee98d63 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 15:48:05 +0200
Subject: [PATCH 051/115] Add is_enabled field so SSOSettings
---
backend/iam/sso/models.py | 7 ++
backend/iam/sso/serializers.py | 157 +++++++++++++++---------------
frontend/src/lib/utils/schemas.ts | 1 +
3 files changed, 86 insertions(+), 79 deletions(-)
diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py
index 988b576c9..b15da2af0 100644
--- a/backend/iam/sso/models.py
+++ b/backend/iam/sso/models.py
@@ -24,6 +24,7 @@ def _fetch_all(self):
created_at=_settings.created_at,
updated_at=_settings.updated_at,
is_published=_settings.is_published,
+ is_enabled=_settings.value.get("is_enabled"),
provider=_settings.value.get("provider"),
client_id=_settings.value.get("client_id"),
provider_id=_settings.value.get("provider_id"),
@@ -56,6 +57,11 @@ def get_queryset(self):
class SSOSettings(GlobalSettings):
objects = SSOSettingsManager()
+ is_enabled = models.BooleanField(
+ verbose_name=_("is enabled"),
+ default=False,
+ )
+
provider = models.CharField(
verbose_name=_("provider"),
max_length=30,
@@ -70,6 +76,7 @@ class SSOSettings(GlobalSettings):
verbose_name=_("client id"),
max_length=191,
help_text=_("App ID, or consumer key"),
+ default="0",
)
secret = models.CharField(
verbose_name=_("secret key"),
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index c913d8f97..c291947f8 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -18,6 +18,9 @@ class Meta:
class SSOSettingsWriteSerializer(BaseModelSerializer):
+ is_enabled = serializers.BooleanField(
+ required=False,
+ )
provider = serializers.CharField(
required=False,
allow_blank=True,
@@ -183,87 +186,83 @@ class Meta:
exclude = ["value"]
def update(self, instance, validated_data):
- # if validated_data.get("provider") == "saml":
- # settings = self.build_saml_settings(validated_data)
- # validated_data["settings"] = settings
settings_object = GlobalSettings.objects.get(name=GlobalSettings.Names.SSO)
settings_object.value = validated_data
- # print(settings_object.value)
settings_object.save()
return instance
- def build_saml_settings(self, validated_data):
- default_attribute_mapping = SAMLProvider.default_attribute_mapping
- attribute_mapping = {
- "uid": validated_data.pop("attribute_mapping_uid", None),
- "email_verified": validated_data.pop(
- "attribute_mapping_email_verified", None
- ),
- "email": validated_data.pop("attribute_mapping_email", None),
- }
- return {
- "attribute_mapping": {
- key: value if value is not None else default_attribute_mapping[key]
- for key, value in attribute_mapping.items()
- },
- "idp": {
- "entity_id": validated_data.pop("idp_entity_id", ""),
- "metadata_url": validated_data.pop("metadata_url", ""),
- "sso_url": validated_data.pop("sso_url", ""),
- "slo_url": validated_data.pop("slo_url", ""),
- "x509cert": validated_data.pop("x509cert", ""),
- },
- "sp": {
- # Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
- "entity_id": validated_data.pop("sp_entity_id", ""),
- },
- # Advanced settings.
- "advanced": {
- "allow_repeat_attribute_name": validated_data.pop(
- "allow_repeat_attribute_name", True
- ),
- "allow_single_label_domains": validated_data.pop(
- "allow_single_label_domains", False
- ),
- "authn_request_signed": validated_data.pop(
- "authn_request_signed", False
- ),
- "digest_algorithm": validated_data.pop(
- "digest_algorithm",
- "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- ),
- "logout_request_signed": validated_data.pop(
- "logout_request_signed", False
- ),
- "logout_response_signed": validated_data.pop(
- "logout_response_signed", False
- ),
- "metadata_signed": validated_data.pop("metadata_signed", False),
- "name_id_encrypted": validated_data.pop("name_id_encrypted", False),
- "reject_deprecated_algorithm": validated_data.pop(
- "reject_deprecated_algorithm", True
- ),
- # Due to security concerns, IdP initiated SSO is rejected by default.
- "reject_idp_initiated_sso": validated_data.pop(
- "reject_idp_initiated_sso", False
- ),
- "signature_algorithm": validated_data.pop(
- "signature_algorithm",
- "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- ),
- "want_assertion_encrypted": validated_data.pop(
- "want_assertion_encrypted", False
- ),
- "want_assertion_signed": validated_data.pop(
- "want_assertion_signed", False
- ),
- "want_attribute_statement": validated_data.pop(
- "want_attribute_statement", True
- ),
- "want_message_signed": validated_data.pop("want_message_signed", False),
- "want_name_id": validated_data.pop("want_name_id", False),
- "want_name_id_encrypted": validated_data.pop(
- "want_name_id_encrypted", False
- ),
- },
- }
+ # def build_saml_settings(self, validated_data):
+ # default_attribute_mapping = SAMLProvider.default_attribute_mapping
+ # attribute_mapping = {
+ # "uid": validated_data.pop("attribute_mapping_uid", None),
+ # "email_verified": validated_data.pop(
+ # "attribute_mapping_email_verified", None
+ # ),
+ # "email": validated_data.pop("attribute_mapping_email", None),
+ # }
+ # return {
+ # "attribute_mapping": {
+ # key: value if value is not None else default_attribute_mapping[key]
+ # for key, value in attribute_mapping.items()
+ # },
+ # "idp": {
+ # "entity_id": validated_data.pop("idp_entity_id", ""),
+ # "metadata_url": validated_data.pop("metadata_url", ""),
+ # "sso_url": validated_data.pop("sso_url", ""),
+ # "slo_url": validated_data.pop("slo_url", ""),
+ # "x509cert": validated_data.pop("x509cert", ""),
+ # },
+ # "sp": {
+ # # Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
+ # "entity_id": validated_data.pop("sp_entity_id", "ciso-assistant"),
+ # },
+ # # Advanced settings.
+ # "advanced": {
+ # "allow_repeat_attribute_name": validated_data.pop(
+ # "allow_repeat_attribute_name", True
+ # ),
+ # "allow_single_label_domains": validated_data.pop(
+ # "allow_single_label_domains", False
+ # ),
+ # "authn_request_signed": validated_data.pop(
+ # "authn_request_signed", False
+ # ),
+ # "digest_algorithm": validated_data.pop(
+ # "digest_algorithm",
+ # "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ # ),
+ # "logout_request_signed": validated_data.pop(
+ # "logout_request_signed", False
+ # ),
+ # "logout_response_signed": validated_data.pop(
+ # "logout_response_signed", False
+ # ),
+ # "metadata_signed": validated_data.pop("metadata_signed", False),
+ # "name_id_encrypted": validated_data.pop("name_id_encrypted", False),
+ # "reject_deprecated_algorithm": validated_data.pop(
+ # "reject_deprecated_algorithm", True
+ # ),
+ # # Due to security concerns, IdP initiated SSO is rejected by default.
+ # "reject_idp_initiated_sso": validated_data.pop(
+ # "reject_idp_initiated_sso", False
+ # ),
+ # "signature_algorithm": validated_data.pop(
+ # "signature_algorithm",
+ # "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ # ),
+ # "want_assertion_encrypted": validated_data.pop(
+ # "want_assertion_encrypted", False
+ # ),
+ # "want_assertion_signed": validated_data.pop(
+ # "want_assertion_signed", False
+ # ),
+ # "want_attribute_statement": validated_data.pop(
+ # "want_attribute_statement", True
+ # ),
+ # "want_message_signed": validated_data.pop("want_message_signed", False),
+ # "want_name_id": validated_data.pop("want_name_id", False),
+ # "want_name_id_encrypted": validated_data.pop(
+ # "want_name_id_encrypted", False
+ # ),
+ # },
+ # }
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index 1abdf9d43..309102105 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -212,6 +212,7 @@ export const EvidenceSchema = baseNamedObject({
});
export const IdentityProviderSchema = z.object({
+ is_enabled: z.boolean().optional(),
provider: z.string(),
provider_id: z.string().optional(),
provider_name: z.string(),
From 2bf19bf7540a7e3b6643546544b955050ceb7462 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 15:48:21 +0200
Subject: [PATCH 052/115] Properly add form fields with prop hidden
---
frontend/src/lib/components/Forms/TextArea.svelte | 2 +-
frontend/src/lib/components/Forms/TextField.svelte | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/lib/components/Forms/TextArea.svelte b/frontend/src/lib/components/Forms/TextArea.svelte
index 80ddf9178..90699cf43 100644
--- a/frontend/src/lib/components/Forms/TextArea.svelte
+++ b/frontend/src/lib/components/Forms/TextArea.svelte
@@ -19,7 +19,7 @@
- {#if label !== undefined}
+ {#if label !== undefined && !$$props.hidden}
{#if $constraints?.required}
- {#if label !== undefined}
+ {#if label !== undefined && !$$props.hidden}
{#if $constraints?.required}
Date: Wed, 19 Jun 2024 15:48:33 +0200
Subject: [PATCH 053/115] Add +layout.svelte
---
frontend/src/routes/(app)/settings/+layout.svelte | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 frontend/src/routes/(app)/settings/+layout.svelte
diff --git a/frontend/src/routes/(app)/settings/+layout.svelte b/frontend/src/routes/(app)/settings/+layout.svelte
new file mode 100644
index 000000000..dc5a1acab
--- /dev/null
+++ b/frontend/src/routes/(app)/settings/+layout.svelte
@@ -0,0 +1,3 @@
+
+
+
From 45bc2438b62c20ef2e7dc77db0b0b3b44999a398 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 15:49:13 +0200
Subject: [PATCH 054/115] Display relevant fields and localize SSOSettings
model form
---
frontend/messages/en.json | 39 +++++-
.../src/lib/components/Forms/ModelForm.svelte | 132 ++++++++++++------
2 files changed, 125 insertions(+), 46 deletions(-)
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 0e003bb97..0c0d237a6 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -562,13 +562,46 @@
"evidenceNoFile": "Evidence has no file uploaded",
"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.",
- "providerId": "Provider ID",
- "clientId": "Client ID",
+ "providerID": "Provider ID",
+ "clientID": "Client ID",
"secret": "Secret",
"key": "Key",
"settings": "Settings",
"identityProvider": "Identity provider",
"identityProviders": "Identity providers",
"addIdentityProvider": "Add identity provider",
- "globalSettings": "Global settings"
+ "globalSettings": "Global settings",
+ "clientIDHelpText": "App ID, or consumer key",
+ "secretHelpText": "API secret, client secret, or consumer secret",
+ "SAMLIdPConfiguration": "SAML IdP configuration",
+ "SPConfiguration": "SP configuration",
+ "advancedSettings": "Advanced settings",
+ "IdPEntityID": "IdP Entity ID",
+ "metadataURL": "Metadata URL",
+ "SSOURL": "SSO URL",
+ "SLOURL": "SLO URL",
+ "x509Cert": "x509 certificate",
+ "SPEntityID": "SP Entity ID",
+ "attributeMappingUID": "Attribute mapping UID",
+ "attributeMappingEmail": "Attribute mapping email",
+ "attributeMappingEmailVerified": "Attribute mapping email verified",
+ "allowRepeatAttributeName": "Allow repeat attribute name",
+ "allowSingleLabelDomains": "Allow single label domains",
+ "authnRequestSigned": "Authn request signed",
+ "digestAlgorithm": "Digest algorithm",
+ "logoutRequestSigned": "Logout request signed",
+ "logoutResponseSigned": "Logout response signed",
+ "metadataSigned": "Metadata signed",
+ "nameIDEncrypted": "Name ID encrypted",
+ "rejectDeprecatedAlgorithm": "Reject deprecated algorithm",
+ "rejectIdPInitiatedSSO": "Reject IdP initiated SSO",
+ "signatureAlgorithm": "Signature algorithm",
+ "wantAssertionSigned": "Want assertion signed",
+ "wantAssertionEncrypted": "Want assertion encrypted",
+ "wantAttributeStatement": "Want attribute statement",
+ "wantMessageSigned": "Want message signed",
+ "wantNameID": "Want name ID",
+ "wantNameIDEncrypted": "Want name ID encrypted",
+ "IdPConfiguration": "IdP configuration",
+ "enableSSO": "Enable SSO"
}
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 95c60cd0a..67ffcb7b8 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -21,6 +21,7 @@
import * as m from '$paraglide/messages.js';
import { zod } from 'sveltekit-superforms/adapters';
import { getSecureRedirect } from '$lib/utils/helpers';
+ import { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
export let form: SuperValidated;
export let model: ModelInfo;
@@ -426,52 +427,97 @@
{/if}
{:else if URLModel === 'identity-providers'}
-
-
-
-
-
-
- {#if data.provider === 'saml'}
- Attribute mapping
-
-
-
+
+
+
+ {#if data.provider !== 'saml'}
+
+ {m.IdPConfiguration()}
+
+
+
+
+ {#if data.provider !== 'saml'}
+
+
+ {/if}
+
+
+ {/if}
+ {#if data.provider === 'saml'}
+
+ {m.SAMLIdPConfiguration()}
+
+
+
+
+
+
+
+
- IdP configuration
-
-
-
-
-
+
+ {m.SPConfiguration()}
+
+
+
+
- SP configuration
-
+ {m.advancedSettings()}
+
+
+
+
- Advanced settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
+
{/if}
{#if closeModal}
From f608269675907cb33d37ad91ee0811916801a518 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Wed, 19 Jun 2024 15:57:28 +0200
Subject: [PATCH 055/115] fix: overwrite is_safe_url
---
backend/ciso_assistant/settings.py | 2 +-
backend/iam/adapter.py | 12 ++++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index 77b954d21..e54f7de75 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 = ["127.0.0.1"]
logger.info("ALLOWED_HOSTS: %s", ALLOWED_HOSTS)
CSRF_TRUSTED_ORIGINS = [CISO_ASSISTANT_URL]
LOCAL_STORAGE_DIRECTORY = os.environ.get(
diff --git a/backend/iam/adapter.py b/backend/iam/adapter.py
index c6dc2f762..154769274 100644
--- a/backend/iam/adapter.py
+++ b/backend/iam/adapter.py
@@ -3,12 +3,15 @@
from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
+from allauth.core import context
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
+from django.utils.http import url_has_allowed_host_and_scheme
+from urllib.parse import urlparse
User = get_user_model()
@@ -16,6 +19,15 @@
class MyAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False
+
+ def is_safe_url(self, url):
+
+ allowed_hosts = {urlparse(settings.CISO_ASSISTANT_URL).hostname} | set(settings.ALLOWED_HOSTS)
+
+ if urlparse(url).port:
+ url = url.replace(':' + str(urlparse(url).port), '')
+
+ return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
class MySocialAccountAdapter(DefaultSocialAccountAdapter):
From 1b595ea8652cd4989f48b9b76a835880a1540dff Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 16:05:33 +0200
Subject: [PATCH 056/115] Conditionally require fields on SSOSettings model
form
---
.../src/lib/components/Forms/ModelForm.svelte | 21 ++++++++++++++++---
.../src/lib/components/Forms/TextField.svelte | 2 +-
2 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 67ffcb7b8..0f241e036 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -459,8 +459,18 @@
{m.SAMLIdPConfiguration()}
-
-
+
+
@@ -470,7 +480,12 @@
{m.SPConfiguration()}
-
+
diff --git a/frontend/src/lib/components/Forms/TextField.svelte b/frontend/src/lib/components/Forms/TextField.svelte
index 9fb88dea5..3023840d6 100644
--- a/frontend/src/lib/components/Forms/TextField.svelte
+++ b/frontend/src/lib/components/Forms/TextField.svelte
@@ -19,7 +19,7 @@
{#if label !== undefined && !$$props.hidden}
- {#if $constraints?.required}
+ {#if $constraints?.required || $$props.required}
From c416b22d358aade21519e4dc66872b820fbcff4f Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 16:41:44 +0200
Subject: [PATCH 057/115] Get SSO Info from /settings/sso/info endpoint
---
backend/settings/urls.py | 3 +-
backend/settings/views.py | 24 +++++++++++++++-
.../(authentication)/login/+page.server.ts | 6 ++--
.../(authentication)/login/+page.svelte | 28 +++++++++----------
4 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/backend/settings/urls.py b/backend/settings/urls.py
index 7659d032b..73aab14c9 100644
--- a/backend/settings/urls.py
+++ b/backend/settings/urls.py
@@ -3,7 +3,7 @@
from iam.sso.views import SSOSettingsViewSet
-from .views import GlobalSettingsViewSet
+from .views import GlobalSettingsViewSet, get_sso_info
from .routers import DefaultSettingsRouter
@@ -21,4 +21,5 @@
urlpatterns = [
path(r"", include(router.urls)),
path(r"", include(settings_router.urls)),
+ path(r"sso/info/", get_sso_info, name="get_sso_info"),
]
diff --git a/backend/settings/views.py b/backend/settings/views.py
index 77723c01d..fbb172c6c 100644
--- a/backend/settings/views.py
+++ b/backend/settings/views.py
@@ -1,5 +1,9 @@
-from rest_framework import viewsets
+from rest_framework import permissions, viewsets
+from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
+from ciso_assistant.settings import CISO_ASSISTANT_URL
+
+from iam.sso.models import SSOSettings
from .serializers import GlobalSettingsSerializer
@@ -27,3 +31,21 @@ def update(self, request, *args, **kwargs):
{"detail": "Global settings can only be updated through data migrations."},
status=405,
)
+
+
+@api_view(["GET"])
+@permission_classes([permissions.AllowAny])
+def get_sso_info(request):
+ """
+ API endpoint that returns the CSRF token.
+ """
+ settings = SSOSettings.objects.get()
+ sp_entity_id = settings.settings["sp"].get("entity_id")
+ callback_url = CISO_ASSISTANT_URL + "/"
+ return Response(
+ {
+ "is_enabled": settings.is_enabled,
+ "sp_entity_id": sp_entity_id,
+ "callback_url": callback_url,
+ }
+ )
diff --git a/frontend/src/routes/(authentication)/login/+page.server.ts b/frontend/src/routes/(authentication)/login/+page.server.ts
index 1298ff17d..e825833bb 100644
--- a/frontend/src/routes/(authentication)/login/+page.server.ts
+++ b/frontend/src/routes/(authentication)/login/+page.server.ts
@@ -8,7 +8,7 @@ import { BASE_API_URL } from '$lib/utils/constants';
import { csrfToken } from '$lib/utils/csrf';
import { loginSchema } from '$lib/utils/schemas';
import { setError, superValidate } from 'sveltekit-superforms';
-export const load: PageServerLoad = async ({ request, locals }) => {
+export const load: PageServerLoad = async ({ fetch, request, locals }) => {
// redirect user if already logged in
if (locals.user) {
redirect(302, '/analytics');
@@ -16,7 +16,9 @@ export const load: PageServerLoad = async ({ request, locals }) => {
const form = await superValidate(request, zod(loginSchema));
- return { form };
+ const SSOInfo = await fetch(`${BASE_API_URL}/settings/sso/info/`).then((res) => res.json());
+
+ return { form, SSOInfo };
};
export const actions: Actions = {
diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte
index 9181b1d3e..e9decd54e 100644
--- a/frontend/src/routes/(authentication)/login/+page.svelte
+++ b/frontend/src/routes/(authentication)/login/+page.svelte
@@ -5,7 +5,6 @@
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';
@@ -78,20 +77,19 @@
-
-
- {m.or()}
-
-
-
+ {#if data.SSOInfo.is_enabled}
+
+
+ {m.or()}
+
+
+
+ {/if}
From 85b58ebc431708e0abf56312f3d6f22fb2cdc89c Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 17:10:00 +0200
Subject: [PATCH 058/115] Initialize SSOSettings object on startup
---
backend/settings/apps.py | 55 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/backend/settings/apps.py b/backend/settings/apps.py
index 727634f96..d05ad2664 100644
--- a/backend/settings/apps.py
+++ b/backend/settings/apps.py
@@ -1,6 +1,61 @@
+import os
from django.apps import AppConfig
+from django.db.models.signals import post_migrate
+
+
+def startup(sender: AppConfig, **kwargs):
+ from .models import GlobalSettings
+ from allauth.socialaccount.providers.saml.provider import SAMLProvider
+
+ default_attribute_mapping = SAMLProvider.default_attribute_mapping
+
+ settings = {
+ "attribute_mapping": {
+ "uid": default_attribute_mapping["uid"],
+ "email_verified": default_attribute_mapping["email_verified"],
+ "email": default_attribute_mapping["email"],
+ },
+ "idp": {
+ "entity_id": "",
+ "metadata_url": "",
+ "sso_url": "",
+ "slo_url": "",
+ "x509cert": "",
+ },
+ "sp": {
+ "entity_id": "ciso-assistant",
+ },
+ "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,
+ "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,
+ },
+ }
+
+ GlobalSettings.objects.get_or_create(
+ name=GlobalSettings.Names.SSO, value={"client_id": "0", "settings": settings}
+ )
class SettingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "settings"
+
+ def ready(self):
+ # avoid post_migrate handler if we are in the main, as it interferes with restore
+ if not os.environ.get("RUN_MAIN"):
+ post_migrate.connect(startup, sender=self)
From cdd7cbdfcbf0c40ea74781527ad0ad911f9cbd3e Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Wed, 19 Jun 2024 17:12:29 +0200
Subject: [PATCH 059/115] Do not create SSOSettings object if it already exists
---
backend/settings/apps.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/backend/settings/apps.py b/backend/settings/apps.py
index d05ad2664..4cca9db82 100644
--- a/backend/settings/apps.py
+++ b/backend/settings/apps.py
@@ -46,9 +46,11 @@ def startup(sender: AppConfig, **kwargs):
},
}
- GlobalSettings.objects.get_or_create(
- name=GlobalSettings.Names.SSO, value={"client_id": "0", "settings": settings}
- )
+ if not GlobalSettings.objects.filter(name=GlobalSettings.Names.SSO).exists():
+ GlobalSettings.objects.get_or_create(
+ name=GlobalSettings.Names.SSO,
+ value={"client_id": "0", "settings": settings},
+ )
class SettingsConfig(AppConfig):
From 467f42acb677c2b6536967cc692109db43a59d33 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Wed, 19 Jun 2024 17:25:29 +0200
Subject: [PATCH 060/115] chore: clean settings.py
---
backend/ciso_assistant/settings.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index d454fc1eb..c5fad583c 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -99,8 +99,7 @@ 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 = ["127.0.0.1"]
+ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
logger.info("ALLOWED_HOSTS: %s", ALLOWED_HOSTS)
CSRF_TRUSTED_ORIGINS = [CISO_ASSISTANT_URL]
LOCAL_STORAGE_DIRECTORY = os.environ.get(
@@ -359,12 +358,9 @@ def set_ciso_assistant_url(_, __, event_dict):
HEADLESS_ONLY = True
HEADLESS_FRONTEND_URLS = {
- "socialaccount_login_error": "http://localhost:5173/",
+ "socialaccount_login_error": CISO_ASSISTANT_URL,
}
-ENTITY_ID = os.environ.get("ENTITY_ID", "")
-METADATA_URL = os.environ.get("METADATA_URL", "")
-
SOCIALACCOUNT_PROVIDERS = {
"saml": {
"EMAIL_AUTHENTICATION": True,
From 3710f7bf3a3e1fb9c8f0add0ff08cfadec6f6bce Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Wed, 19 Jun 2024 17:42:39 +0200
Subject: [PATCH 061/115] chore: run format
---
backend/iam/adapter.py | 13 +-
frontend/messages/de.json | 2 +-
frontend/messages/en.json | 2 +-
frontend/messages/es.json | 2 +-
frontend/messages/fr.json | 2 +-
frontend/messages/it.json | 2 +-
frontend/messages/nl.json | 2 +-
frontend/messages/pl.json | 346 +++++++++++++++---------------
frontend/src/lib/utils/locales.ts | 2 +-
9 files changed, 187 insertions(+), 186 deletions(-)
diff --git a/backend/iam/adapter.py b/backend/iam/adapter.py
index a6ca8c03b..0cb062241 100644
--- a/backend/iam/adapter.py
+++ b/backend/iam/adapter.py
@@ -26,14 +26,15 @@
class MyAccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False
-
+
def is_safe_url(self, url):
-
- allowed_hosts = {urlparse(settings.CISO_ASSISTANT_URL).hostname} | set(settings.ALLOWED_HOSTS)
-
+ allowed_hosts = {urlparse(settings.CISO_ASSISTANT_URL).hostname} | set(
+ settings.ALLOWED_HOSTS
+ )
+
if urlparse(url).port:
- url = url.replace(':' + str(urlparse(url).port), '')
-
+ url = url.replace(":" + str(urlparse(url).port), "")
+
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
diff --git a/frontend/messages/de.json b/frontend/messages/de.json
index fa3139639..74c072968 100644
--- a/frontend/messages/de.json
+++ b/frontend/messages/de.json
@@ -8,7 +8,7 @@
"german": "Deutsch",
"dutch": "Niederländisch",
"italian": "Italienisch",
- "polish" :"Polnish",
+ "polish": "Polnish",
"addThreat": "Bedrohung hinzufügen",
"addReferenceControl": "Referenzkontrolle hinzufügen",
"addAppliedControl": "Angewendete Kontrolle hinzufügen",
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index fa02e7464..edabb9815 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -8,7 +8,7 @@
"german": "German",
"dutch": "Dutch",
"italian": "Italian",
- "polish" :"Polish",
+ "polish": "Polish",
"addThreat": "Add threat",
"addReferenceControl": "Add reference control",
"addAppliedControl": "Add applied control",
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index a1fc44001..f0414f91d 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -8,7 +8,7 @@
"german": "Alemán",
"dutch": "Holandés",
"italian": "Italiano",
- "polish" :"Polnish",
+ "polish": "Polnish",
"addThreat": "Agregar amenaza",
"addReferenceControl": "Agregar control de referencia",
"addAppliedControl": "Agregar control aplicado",
diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json
index c399589b0..2d9b0bd2f 100644
--- a/frontend/messages/fr.json
+++ b/frontend/messages/fr.json
@@ -8,7 +8,7 @@
"german": "Allemand",
"dutch": "Néerlandais",
"italian": "Italien",
- "polish" :"Polonais",
+ "polish": "Polonais",
"addThreat": "Ajouter une menace",
"addReferenceControl": "Ajouter une mesure de référence",
"addAppliedControl": "Ajouter une mesure appliquée",
diff --git a/frontend/messages/it.json b/frontend/messages/it.json
index cceac222e..8974654f3 100644
--- a/frontend/messages/it.json
+++ b/frontend/messages/it.json
@@ -8,7 +8,7 @@
"german": "Tedesco",
"dutch": "Olandese",
"italian": "Italiano",
- "polish" :"Polacco",
+ "polish": "Polacco",
"addThreat": "Aggiungi minaccia",
"addReferenceControl": "Aggiungi controllo di riferimento",
"addAppliedControl": "Aggiungi controllo applicato",
diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json
index 8b15736cd..27094e18e 100644
--- a/frontend/messages/nl.json
+++ b/frontend/messages/nl.json
@@ -8,7 +8,7 @@
"german": "Duits",
"dutch": "Nederlands",
"italian": "Italiaans",
- "polish" :"Pools",
+ "polish": "Pools",
"addThreat": "Bedreiging toevoegen",
"addReferenceControl": "Referentiecontrole toevoegen",
"addAppliedControl": "Toegepaste controle toevoegen",
diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json
index 105149650..5246c611e 100644
--- a/frontend/messages/pl.json
+++ b/frontend/messages/pl.json
@@ -1,175 +1,175 @@
{
- "$schema": "https://inlang.com/schema/inlang-message-format",
- "french": "Francuski",
- "english": "Angielski",
- "arabic": "Arabski",
- "portuguese": "Portugalski",
- "spanish": "Hiszpański",
- "german": "Niemiecki",
- "dutch": "Holenderski",
- "italian": "WÅ‚oski",
- "addThreat": "Dodaj zagrożenie",
- "addReferenceControl": "Dodaj kontrolÄ™ referencyjnÄ…",
- "addAppliedControl": "Dodaj zastosowanÄ… kontrolÄ™",
- "addAsset": "Dodaj zasób",
- "addRiskAssessment": "Dodaj ocenÄ™ ryzyka",
- "addRiskScenario": "Dodaj scenariusz ryzyka",
- "addRiskAcceptance": "Dodaj akceptacjÄ™ ryzyka",
- "addComplianceAssessment": "Rozpocznij audyt",
- "addEvidence": "Dodaj dowód",
- "addDomain": "Dodaj domenÄ™",
- "addProject": "Dodaj projekt",
- "addUser": "Dodaj użytkownika",
- "addPolicy": "Dodaj politykÄ™",
- "associatedThreats": "Powiązane zagrożenia",
- "associatedReferenceControls": "PowiÄ…zane kontrole referencyjne",
- "associatedAppliedControls": "PowiÄ…zane zastosowane kontrole",
- "associatedAssets": "PowiÄ…zane zasoby",
- "associatedRiskAssessments": "PowiÄ…zane oceny ryzyka",
- "associatedRiskScenarios": "PowiÄ…zane scenariusze ryzyka",
- "associatedRiskAcceptances": "PowiÄ…zane akceptacje ryzyka",
- "associatedComplianceAssessments": "PowiÄ…zane audyty",
- "associatedEvidences": "PowiÄ…zane dowody",
- "associatedDomains": "PowiÄ…zane domeny",
- "associatedProjects": "PowiÄ…zane projekty",
- "associatedUsers": "Powiązani użytkownicy",
- "home": "Strona główna",
- "edit": "Edytuj",
- "changePassword": "Zmień hasło",
- "overview": "PrzeglÄ…d",
- "context": "Kontekst",
- "governance": "ZarzÄ…dzanie",
- "risk": "Ryzyko",
- "compliance": "Zgodność",
- "organization": "Organizacja",
- "extra": "Dodatkowe",
- "analytics": "Analizy",
- "calendar": "Kalendarz",
- "threats": "Zagrożenia",
- "referenceControls": "Kontrole referencyjne",
- "appliedControls": "Zastosowane kontrole",
- "assets": "Zasoby",
- "asset": "Zasób",
- "policy": "Polityka",
- "policies": "Polityki",
- "riskMatrices": "Macierze ryzyka",
- "riskAssessments": "Oceny ryzyka",
- "riskScenarios": "Scenariusze ryzyka",
- "riskScenario": "Scenariusz ryzyka",
- "riskAcceptances": "Akceptacje ryzyka",
- "riskAcceptance": "Akceptacja ryzyka",
- "complianceAssessments": "Audyty",
- "complianceAssessment": "Audyt",
- "evidences": "Dowody",
- "evidence": "Dowód",
- "frameworks": "Ramy",
- "domains": "Domeny",
- "projects": "Projekty",
- "users": "Użytkownicy",
- "user": "Użytkownik",
- "userGroups": "Grupy użytkowników",
- "roleAssignments": "Przypisania ról",
- "xRays": "Prześwietlenia",
- "scoringAssistant": "Asystent oceny",
- "scoringAssistantNoMatrixError": "Proszę zaimportować macierz ryzyka z biblioteki, aby uzyskać dostęp do tej strony",
- "libraries": "Biblioteki",
- "backupRestore": "Kopia zapasowa i przywracanie",
- "myProfile": "Mój profil",
- "aboutCiso": "O asystencie CISO",
- "Logout": "Wyloguj siÄ™",
- "name": "Nazwa",
- "description": "Opis",
- "parentDomain": "Domena nadrzędna",
- "ref": "Ref",
- "refId": "ID referencyjne",
- "businessValue": "Wartość biznesowa",
- "email": "E-mail",
- "firstName": "ImiÄ™",
- "lastName": "Nazwisko",
- "category": "Kategoria",
- "eta": "ETA",
- "referenceControl": "Kontrola referencyjna",
- "appliedControl": "Zastosowana kontrola",
- "provider": "Dostawca",
- "domain": "Domena",
- "urn": "URN",
- "id": "ID",
- "treatmentStatus": "Status leczenia",
- "currentLevel": "Obecny poziom",
- "residualLevel": "Poziom resztkowy",
- "riskMatrix": "Macierz ryzyka",
- "project": "Projekt",
- "folder": "Folder",
- "riskAssessment": "Ocena ryzyka",
- "threat": "Zagrożenie",
- "framework": "Ramy",
- "file": "Plik",
- "language": "Język",
- "builtin": "Wbudowany",
- "next": "Następny",
- "previous": "Poprzedni",
- "show": "Pokaż",
- "entries": "wpisy",
- "searchPlaceholder": "Szukaj...",
- "noEntriesFound": "Nie znaleziono wpisów",
- "rowCount": "Pokazano {start} do {end} z {total}",
- "status": "Status",
- "effort": "Wysiłek",
- "impact": "Wpływ",
- "expiryDate": "Data wygaśnięcia",
- "link": "Link",
- "createdAt": "Utworzono",
- "updatedAt": "Zaktualizowano",
- "acceptedAt": "Zaakceptowano",
- "rejectedAt": "Odrzucono",
- "revokedAt": "Cofnięto",
- "submitted": "Przesłano",
- "rejected": "Odrzucono",
- "revoked": "Cofnięto",
- "locale": "Lokalizacja",
- "defaultLocale": "Domyślna lokalizacja",
- "annotation": "Adnotacja",
- "library": "Biblioteka",
- "typicalEvidence": "Typowy dowód",
- "parentAsset": "Zasób nadrzędny",
- "parentAssets": "Zasoby nadrzędne",
- "approver": "AkceptujÄ…cy",
- "state": "Stan",
- "justification": "Uzasadnienie",
- "parentFolder": "Folder nadrzędny",
- "contentType": "Rodzaj treści",
- "type": "Typ",
- "lcStatus": "Status LC",
- "internalReference": "Referencja wewnętrzna",
- "isActive": "Aktywny",
- "dateJoined": "Data dołączenia",
- "version": "Wersja",
- "treatment": "Leczenie",
- "currentProba": "Obecne prawdopodobieństwo",
- "currentImpact": "Obecny wpływ",
- "residualProba": "Resztkowe prawdopodobieństwo",
- "residualImpact": "Resztkowy wpływ",
- "existingControls": "IstniejÄ…ce kontrole",
- "strengthOfKnowledge": "Siła wiedzy",
- "dueDate": "Termin",
- "attachment": "Załącznik",
- "observation": "Obserwacja",
- "importMatrices": "Importuj macierze",
- "importFrameworks": "Importuj ramy",
- "summary": "Podsumowanie",
- "composer": "Kompozytor",
- "statistics": "Statystyki",
- "myProjects": "Moje projekty",
- "scenarios": "Scenariusze",
- "assignedProjects": "Przypisane do {number} projektu(ów)",
- "currentRiskLevelPerScenario": "Obecny poziom ryzyka na scenariusz ryzyka",
- "residualRiskLevelPerScenario": "Resztkowy poziom ryzyka na scenariusz ryzyka",
- "appliedControlsStatus": "Status zastosowanych kontroli",
- "currentRisk": "Obecne ryzyko",
- "residualRisk": "Resztkowe ryzyko",
- "planned": "Planowane",
- "active": "Aktywne",
- "inactive": "Nieaktywne",
- "watchlist": "Lista obserwacyjna",
- "watchlistDescription": "Elementy, które wygasły lub wygasną w ciągu najbliższych 30 dni"
+ "$schema": "https://inlang.com/schema/inlang-message-format",
+ "french": "Francuski",
+ "english": "Angielski",
+ "arabic": "Arabski",
+ "portuguese": "Portugalski",
+ "spanish": "Hiszpański",
+ "german": "Niemiecki",
+ "dutch": "Holenderski",
+ "italian": "WÅ‚oski",
+ "addThreat": "Dodaj zagrożenie",
+ "addReferenceControl": "Dodaj kontrolÄ™ referencyjnÄ…",
+ "addAppliedControl": "Dodaj zastosowanÄ… kontrolÄ™",
+ "addAsset": "Dodaj zasób",
+ "addRiskAssessment": "Dodaj ocenÄ™ ryzyka",
+ "addRiskScenario": "Dodaj scenariusz ryzyka",
+ "addRiskAcceptance": "Dodaj akceptacjÄ™ ryzyka",
+ "addComplianceAssessment": "Rozpocznij audyt",
+ "addEvidence": "Dodaj dowód",
+ "addDomain": "Dodaj domenÄ™",
+ "addProject": "Dodaj projekt",
+ "addUser": "Dodaj użytkownika",
+ "addPolicy": "Dodaj politykÄ™",
+ "associatedThreats": "Powiązane zagrożenia",
+ "associatedReferenceControls": "PowiÄ…zane kontrole referencyjne",
+ "associatedAppliedControls": "PowiÄ…zane zastosowane kontrole",
+ "associatedAssets": "PowiÄ…zane zasoby",
+ "associatedRiskAssessments": "PowiÄ…zane oceny ryzyka",
+ "associatedRiskScenarios": "PowiÄ…zane scenariusze ryzyka",
+ "associatedRiskAcceptances": "PowiÄ…zane akceptacje ryzyka",
+ "associatedComplianceAssessments": "PowiÄ…zane audyty",
+ "associatedEvidences": "PowiÄ…zane dowody",
+ "associatedDomains": "PowiÄ…zane domeny",
+ "associatedProjects": "PowiÄ…zane projekty",
+ "associatedUsers": "Powiązani użytkownicy",
+ "home": "Strona główna",
+ "edit": "Edytuj",
+ "changePassword": "Zmień hasło",
+ "overview": "PrzeglÄ…d",
+ "context": "Kontekst",
+ "governance": "ZarzÄ…dzanie",
+ "risk": "Ryzyko",
+ "compliance": "Zgodność",
+ "organization": "Organizacja",
+ "extra": "Dodatkowe",
+ "analytics": "Analizy",
+ "calendar": "Kalendarz",
+ "threats": "Zagrożenia",
+ "referenceControls": "Kontrole referencyjne",
+ "appliedControls": "Zastosowane kontrole",
+ "assets": "Zasoby",
+ "asset": "Zasób",
+ "policy": "Polityka",
+ "policies": "Polityki",
+ "riskMatrices": "Macierze ryzyka",
+ "riskAssessments": "Oceny ryzyka",
+ "riskScenarios": "Scenariusze ryzyka",
+ "riskScenario": "Scenariusz ryzyka",
+ "riskAcceptances": "Akceptacje ryzyka",
+ "riskAcceptance": "Akceptacja ryzyka",
+ "complianceAssessments": "Audyty",
+ "complianceAssessment": "Audyt",
+ "evidences": "Dowody",
+ "evidence": "Dowód",
+ "frameworks": "Ramy",
+ "domains": "Domeny",
+ "projects": "Projekty",
+ "users": "Użytkownicy",
+ "user": "Użytkownik",
+ "userGroups": "Grupy użytkowników",
+ "roleAssignments": "Przypisania ról",
+ "xRays": "Prześwietlenia",
+ "scoringAssistant": "Asystent oceny",
+ "scoringAssistantNoMatrixError": "Proszę zaimportować macierz ryzyka z biblioteki, aby uzyskać dostęp do tej strony",
+ "libraries": "Biblioteki",
+ "backupRestore": "Kopia zapasowa i przywracanie",
+ "myProfile": "Mój profil",
+ "aboutCiso": "O asystencie CISO",
+ "Logout": "Wyloguj siÄ™",
+ "name": "Nazwa",
+ "description": "Opis",
+ "parentDomain": "Domena nadrzędna",
+ "ref": "Ref",
+ "refId": "ID referencyjne",
+ "businessValue": "Wartość biznesowa",
+ "email": "E-mail",
+ "firstName": "ImiÄ™",
+ "lastName": "Nazwisko",
+ "category": "Kategoria",
+ "eta": "ETA",
+ "referenceControl": "Kontrola referencyjna",
+ "appliedControl": "Zastosowana kontrola",
+ "provider": "Dostawca",
+ "domain": "Domena",
+ "urn": "URN",
+ "id": "ID",
+ "treatmentStatus": "Status leczenia",
+ "currentLevel": "Obecny poziom",
+ "residualLevel": "Poziom resztkowy",
+ "riskMatrix": "Macierz ryzyka",
+ "project": "Projekt",
+ "folder": "Folder",
+ "riskAssessment": "Ocena ryzyka",
+ "threat": "Zagrożenie",
+ "framework": "Ramy",
+ "file": "Plik",
+ "language": "Język",
+ "builtin": "Wbudowany",
+ "next": "Następny",
+ "previous": "Poprzedni",
+ "show": "Pokaż",
+ "entries": "wpisy",
+ "searchPlaceholder": "Szukaj...",
+ "noEntriesFound": "Nie znaleziono wpisów",
+ "rowCount": "Pokazano {start} do {end} z {total}",
+ "status": "Status",
+ "effort": "Wysiłek",
+ "impact": "Wpływ",
+ "expiryDate": "Data wygaśnięcia",
+ "link": "Link",
+ "createdAt": "Utworzono",
+ "updatedAt": "Zaktualizowano",
+ "acceptedAt": "Zaakceptowano",
+ "rejectedAt": "Odrzucono",
+ "revokedAt": "Cofnięto",
+ "submitted": "Przesłano",
+ "rejected": "Odrzucono",
+ "revoked": "Cofnięto",
+ "locale": "Lokalizacja",
+ "defaultLocale": "Domyślna lokalizacja",
+ "annotation": "Adnotacja",
+ "library": "Biblioteka",
+ "typicalEvidence": "Typowy dowód",
+ "parentAsset": "Zasób nadrzędny",
+ "parentAssets": "Zasoby nadrzędne",
+ "approver": "AkceptujÄ…cy",
+ "state": "Stan",
+ "justification": "Uzasadnienie",
+ "parentFolder": "Folder nadrzędny",
+ "contentType": "Rodzaj treści",
+ "type": "Typ",
+ "lcStatus": "Status LC",
+ "internalReference": "Referencja wewnętrzna",
+ "isActive": "Aktywny",
+ "dateJoined": "Data dołączenia",
+ "version": "Wersja",
+ "treatment": "Leczenie",
+ "currentProba": "Obecne prawdopodobieństwo",
+ "currentImpact": "Obecny wpływ",
+ "residualProba": "Resztkowe prawdopodobieństwo",
+ "residualImpact": "Resztkowy wpływ",
+ "existingControls": "IstniejÄ…ce kontrole",
+ "strengthOfKnowledge": "Siła wiedzy",
+ "dueDate": "Termin",
+ "attachment": "Załącznik",
+ "observation": "Obserwacja",
+ "importMatrices": "Importuj macierze",
+ "importFrameworks": "Importuj ramy",
+ "summary": "Podsumowanie",
+ "composer": "Kompozytor",
+ "statistics": "Statystyki",
+ "myProjects": "Moje projekty",
+ "scenarios": "Scenariusze",
+ "assignedProjects": "Przypisane do {number} projektu(ów)",
+ "currentRiskLevelPerScenario": "Obecny poziom ryzyka na scenariusz ryzyka",
+ "residualRiskLevelPerScenario": "Resztkowy poziom ryzyka na scenariusz ryzyka",
+ "appliedControlsStatus": "Status zastosowanych kontroli",
+ "currentRisk": "Obecne ryzyko",
+ "residualRisk": "Resztkowe ryzyko",
+ "planned": "Planowane",
+ "active": "Aktywne",
+ "inactive": "Nieaktywne",
+ "watchlist": "Lista obserwacyjna",
+ "watchlistDescription": "Elementy, które wygasły lub wygasną w ciągu najbliższych 30 dni"
}
diff --git a/frontend/src/lib/utils/locales.ts b/frontend/src/lib/utils/locales.ts
index 8d3d36ac8..18ed9a792 100644
--- a/frontend/src/lib/utils/locales.ts
+++ b/frontend/src/lib/utils/locales.ts
@@ -63,7 +63,7 @@ export function localItems(): LocalItems {
german: m.german(),
dutch: m.dutch(),
italian: m.italian(),
- polish:m.polish(),
+ polish: m.polish(),
home: m.home(),
edit: m.edit(),
overview: m.overview(),
From c48adf5eaf67067dedcbebb17e798bdf41e2b9a3 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Wed, 19 Jun 2024 18:20:30 +0200
Subject: [PATCH 062/115] feat: catch errors and cleaning
---
backend/ciso_assistant/settings.py | 4 +-
backend/iam/sso/urls.py | 3 +-
backend/iam/sso/views.py | 28 ++++++
frontend/src/hooks.server.ts | 2 +-
frontend/src/lib/allauth.js | 99 -------------------
frontend/src/lib/allauth.ts | 38 +++++++
frontend/src/lib/{django.js => django.ts} | 0
.../(authentication)/login/+page.svelte | 2 +-
8 files changed, 72 insertions(+), 104 deletions(-)
delete mode 100644 frontend/src/lib/allauth.js
create mode 100644 frontend/src/lib/allauth.ts
rename frontend/src/lib/{django.js => django.ts} (100%)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index c5fad583c..132892c57 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -4,7 +4,7 @@
CORS are not managed by backend, so CORS library is not used
if "POSTGRES_NAME" environment variable defined, the database engine is posgresql
-and the other env variables are POSGRES_USER, POSTGRES_PASSWORD, DB_HOST, DB_PORT
+and the other env variables are POSTGRES_USER, POSTGRES_PASSWORD, DB_HOST, DB_PORT
else it is sqlite, and no env variable is required
"""
@@ -358,7 +358,7 @@ def set_ciso_assistant_url(_, __, event_dict):
HEADLESS_ONLY = True
HEADLESS_FRONTEND_URLS = {
- "socialaccount_login_error": CISO_ASSISTANT_URL,
+ "socialaccount_login_error": CISO_ASSISTANT_URL + "/login",
}
SOCIALACCOUNT_PROVIDERS = {
diff --git a/backend/iam/sso/urls.py b/backend/iam/sso/urls.py
index eb932d935..01c7acd18 100644
--- a/backend/iam/sso/urls.py
+++ b/backend/iam/sso/urls.py
@@ -1,7 +1,7 @@
from django.urls import include, path
from rest_framework import routers
-from .views import SSOSettingsViewSet
+from .views import SSOSettingsViewSet, RedirectToProviderView
router = routers.DefaultRouter()
@@ -9,4 +9,5 @@
urlpatterns = [
path("", include(router.urls)),
+ path("redirect", RedirectToProviderView.as_view(), name="sso-redirect"),
]
diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py
index e944f7d12..14956c8c2 100644
--- a/backend/iam/sso/views.py
+++ b/backend/iam/sso/views.py
@@ -4,6 +4,34 @@
from .serializers import SSOSettingsReadSerializer, SSOSettingsWriteSerializer
from rest_framework.decorators import action
from allauth.socialaccount import providers
+from allauth.headless.base.views import APIView
+from allauth.headless.socialaccount.forms import RedirectToProviderForm
+from allauth.socialaccount.providers.saml.views import render_authentication_error
+
+
+class RedirectToProviderView(APIView):
+ handle_json_input = False
+
+ def post(self, request, *args, **kwargs):
+ form = RedirectToProviderForm(request.POST)
+ if not form.is_valid():
+ return render_authentication_error(
+ request,
+ provider=request.POST.get("provider"),
+ exception=ValidationError(form.errors),
+ )
+ provider = form.cleaned_data["provider"]
+ next_url = form.cleaned_data["callback_url"]
+ process = form.cleaned_data["process"]
+ try:
+ return provider.redirect(
+ request,
+ process,
+ next_url=next_url,
+ headless=True,
+ )
+ except:
+ return render_authentication_error(request, provider, error="failedSSO")
class BaseModelViewSet(AbstractBaseModelViewSet):
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts
index 690e7ba55..d88bbeb6b 100644
--- a/frontend/src/hooks.server.ts
+++ b/frontend/src/hooks.server.ts
@@ -54,7 +54,7 @@ export const handle: Handle = async ({ event, resolve }) => {
if (errorId) {
setLanguageTag(event.cookies.get('ciso_lang'));
setFlash({ type: 'error', message: m.failedSSO() }, event);
- redirect(302, `/`);
+ redirect(302, '/login');
}
const user = await validateUserSession(event);
diff --git a/frontend/src/lib/allauth.js b/frontend/src/lib/allauth.js
deleted file mode 100644
index df7ca8df1..000000000
--- a/frontend/src/lib/allauth.js
+++ /dev/null
@@ -1,99 +0,0 @@
-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/allauth.ts b/frontend/src/lib/allauth.ts
new file mode 100644
index 000000000..99d0d05c7
--- /dev/null
+++ b/frontend/src/lib/allauth.ts
@@ -0,0 +1,38 @@
+import { getCSRFToken } from './django.js';
+import { BASE_API_URL } from '$lib/utils/constants';
+
+const BASE_URL = `${BASE_API_URL}`;
+
+export const AuthProcess = Object.freeze({
+ LOGIN: 'login',
+ CONNECT: 'connect'
+});
+
+export const URLs = Object.freeze({
+ REDIRECT_TO_PROVIDER: BASE_URL + '/iam/sso/redirect'
+});
+
+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.ts
similarity index 100%
rename from frontend/src/lib/django.js
rename to frontend/src/lib/django.ts
diff --git a/frontend/src/routes/(authentication)/login/+page.svelte b/frontend/src/routes/(authentication)/login/+page.svelte
index e9decd54e..919f094b6 100644
--- a/frontend/src/routes/(authentication)/login/+page.svelte
+++ b/frontend/src/routes/(authentication)/login/+page.svelte
@@ -8,7 +8,7 @@
import * as m from '$paraglide/messages.js';
import { zod } from 'sveltekit-superforms/adapters';
- import { redirectToProvider } from '$lib/allauth';
+ import { redirectToProvider } from '$lib/allauth.js';
export let data: PageData;
From 2bd6d057ae022c2c52696e47daed8bfe43b6a75b Mon Sep 17 00:00:00 2001
From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
Date: Wed, 19 Jun 2024 23:13:51 +0200
Subject: [PATCH 063/115] Add start-caddy.sh
script to create Caddyfile dynamically based on CISO_ASSISTANT_URL and launch caddy for saml compatibility
---
README.md | 2 +-
start-caddy.sh | 17 +++++++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
create mode 100755 start-caddy.sh
diff --git a/README.md b/README.md
index abf440e5c..60b7a109c 100644
--- a/README.md
+++ b/README.md
@@ -442,7 +442,7 @@ The docker-compose.yml highlights a relevant configuration with a Caddy proxy in
Set DJANGO_DEBUG=False for security reason.
> [!NOTE]
-> The frontend cannot infer the host automatically, so you need to either set the ORIGIN variable, or the HOST_HEADER and PROTOCOL_HEADER variables. Please see [the sveltekit doc](https://kit.svelte.dev/docs/adapter-node#environment-variables-origin-protocolheader-hostheader-and-port-header) on this tricky issue.
+> The frontend cannot infer the host automatically, so you need to either set the ORIGIN variable, or the HOST_HEADER and PROTOCOL_HEADER variables. Please see [the sveltekit doc](https://kit.svelte.dev/docs/adapter-node#environment-variables-origin-protocolheader-hostheader-and-port-header) on this tricky issue. Beware that this approach does not work with "npm run dev", which should not be a worry for production.
> [!NOTE]
> Caddy needs to receive a SNI header. Therefore, for your public URL (the one declared in CISO_ASSISTANT_URL), you need to use a FQDN, not an IP address, as the SNI is not transmitted by a browser if the host is an IP address. Another tricky issue!
diff --git a/start-caddy.sh b/start-caddy.sh
new file mode 100755
index 000000000..6074692e9
--- /dev/null
+++ b/start-caddy.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# create Caddyfile and start caddy
+
+python -c "
+import os, re, pathlib
+url = os.environ.get('CISO_ASSISTANT_URL')
+assert url, 'missing environment variable CISO_ASSISTANT_URL'
+q=re.match(r'https://([^/]+)', url)
+assert q, 'bad environment variable CISO_ASSISTANT_URL'
+target=q.group(1)
+a = target + ''' {
+ reverse_proxy /accounts/saml/acs/ localhost:8000
+ reverse_proxy /accounts/saml/acs/finish/ localhost:8000
+ reverse_proxy /* localhost:3000
+}'''
+pathlib.Path('Caddyfile').write_text(a)
+" && caddy run
From 22d412f3b8c2aad10530be04aad80da64f2f30f4 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 09:37:13 +0200
Subject: [PATCH 064/115] feat: rename settings app by global_settings
---
backend/ciso_assistant/settings.py | 2 +-
backend/core/urls.py | 2 +-
backend/{settings => global_settings}/__init__.py | 0
backend/{settings => global_settings}/apps.py | 2 +-
.../{settings => global_settings}/migrations/0001_initial.py | 0
.../migrations/0002_alter_globalsettings_value.py | 2 +-
backend/{settings => global_settings}/migrations/__init__.py | 0
backend/{settings => global_settings}/models.py | 0
backend/{settings => global_settings}/routers.py | 0
backend/{settings => global_settings}/serializers.py | 0
backend/{settings => global_settings}/urls.py | 0
backend/{settings => global_settings}/views.py | 0
backend/iam/migrations/0004_ssosettings.py | 4 ++--
backend/iam/sso/models.py | 2 +-
backend/iam/sso/serializers.py | 2 +-
15 files changed, 8 insertions(+), 8 deletions(-)
rename backend/{settings => global_settings}/__init__.py (100%)
rename backend/{settings => global_settings}/apps.py (98%)
rename backend/{settings => global_settings}/migrations/0001_initial.py (100%)
rename backend/{settings => global_settings}/migrations/0002_alter_globalsettings_value.py (88%)
rename backend/{settings => global_settings}/migrations/__init__.py (100%)
rename backend/{settings => global_settings}/models.py (100%)
rename backend/{settings => global_settings}/routers.py (100%)
rename backend/{settings => global_settings}/serializers.py (100%)
rename backend/{settings => global_settings}/urls.py (100%)
rename backend/{settings => global_settings}/views.py (100%)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index 132892c57..dd6a632dd 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -123,7 +123,7 @@ def set_ciso_assistant_url(_, __, event_dict):
"tailwind",
"iam",
"core",
- "settings",
+ "global_settings",
"cal",
"django_filters",
# "debug_toolbar",
diff --git a/backend/core/urls.py b/backend/core/urls.py
index 3c89c2581..67c023b3b 100644
--- a/backend/core/urls.py
+++ b/backend/core/urls.py
@@ -52,7 +52,7 @@
path("", include(router.urls)),
path("iam/", include("iam.urls")),
path("serdes/", include("serdes.urls")),
- path("settings/", include("settings.urls")),
+ path("settings/", include("global_settings.urls")),
path("csrf/", get_csrf_token, name="get_csrf_token"),
path("build/", get_build, name="get_build"),
path("license/", license, name="license"),
diff --git a/backend/settings/__init__.py b/backend/global_settings/__init__.py
similarity index 100%
rename from backend/settings/__init__.py
rename to backend/global_settings/__init__.py
diff --git a/backend/settings/apps.py b/backend/global_settings/apps.py
similarity index 98%
rename from backend/settings/apps.py
rename to backend/global_settings/apps.py
index 4cca9db82..ea4907f5d 100644
--- a/backend/settings/apps.py
+++ b/backend/global_settings/apps.py
@@ -55,7 +55,7 @@ def startup(sender: AppConfig, **kwargs):
class SettingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
- name = "settings"
+ name = "global_settings"
def ready(self):
# avoid post_migrate handler if we are in the main, as it interferes with restore
diff --git a/backend/settings/migrations/0001_initial.py b/backend/global_settings/migrations/0001_initial.py
similarity index 100%
rename from backend/settings/migrations/0001_initial.py
rename to backend/global_settings/migrations/0001_initial.py
diff --git a/backend/settings/migrations/0002_alter_globalsettings_value.py b/backend/global_settings/migrations/0002_alter_globalsettings_value.py
similarity index 88%
rename from backend/settings/migrations/0002_alter_globalsettings_value.py
rename to backend/global_settings/migrations/0002_alter_globalsettings_value.py
index 4df3cd05f..15b296eda 100644
--- a/backend/settings/migrations/0002_alter_globalsettings_value.py
+++ b/backend/global_settings/migrations/0002_alter_globalsettings_value.py
@@ -5,7 +5,7 @@
class Migration(migrations.Migration):
dependencies = [
- ("settings", "0001_initial"),
+ ("global_settings", "0001_initial"),
]
operations = [
diff --git a/backend/settings/migrations/__init__.py b/backend/global_settings/migrations/__init__.py
similarity index 100%
rename from backend/settings/migrations/__init__.py
rename to backend/global_settings/migrations/__init__.py
diff --git a/backend/settings/models.py b/backend/global_settings/models.py
similarity index 100%
rename from backend/settings/models.py
rename to backend/global_settings/models.py
diff --git a/backend/settings/routers.py b/backend/global_settings/routers.py
similarity index 100%
rename from backend/settings/routers.py
rename to backend/global_settings/routers.py
diff --git a/backend/settings/serializers.py b/backend/global_settings/serializers.py
similarity index 100%
rename from backend/settings/serializers.py
rename to backend/global_settings/serializers.py
diff --git a/backend/settings/urls.py b/backend/global_settings/urls.py
similarity index 100%
rename from backend/settings/urls.py
rename to backend/global_settings/urls.py
diff --git a/backend/settings/views.py b/backend/global_settings/views.py
similarity index 100%
rename from backend/settings/views.py
rename to backend/global_settings/views.py
diff --git a/backend/iam/migrations/0004_ssosettings.py b/backend/iam/migrations/0004_ssosettings.py
index b227bedb9..aa6e3d6eb 100644
--- a/backend/iam/migrations/0004_ssosettings.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -6,7 +6,7 @@
class Migration(migrations.Migration):
dependencies = [
("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"),
- ("settings", "0001_initial"),
+ ("global_settings", "0001_initial"),
]
operations = [
@@ -53,6 +53,6 @@ class Migration(migrations.Migration):
"managed": False,
"proxy": True,
},
- bases=("settings.globalsettings",),
+ bases=("global_settings.globalsettings",),
),
]
diff --git a/backend/iam/sso/models.py b/backend/iam/sso/models.py
index b15da2af0..c63d73713 100644
--- a/backend/iam/sso/models.py
+++ b/backend/iam/sso/models.py
@@ -4,7 +4,7 @@
from django.db.models.query import QuerySet
from allauth.socialaccount.models import providers
-from settings.models import GlobalSettings
+from global_settings.models import GlobalSettings
class SSOSettingsQuerySet(QuerySet):
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index c291947f8..89eace686 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -1,7 +1,7 @@
from allauth.socialaccount.providers.saml.provider import SAMLProvider
from rest_framework import serializers
-from settings.models import GlobalSettings
+from global_settings.models import GlobalSettings
from .models import SSOSettings
from core.serializers import BaseModelSerializer
From dd56b8a1125eeb6eda4fa862cd49b2ac0a52673d Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 15:57:44 +0200
Subject: [PATCH 065/115] feat: add sso endpoints for proxy
---
.gitignore | 1 +
Caddyfile => Caddyfile.example | 0
backend/ciso_assistant/settings.py | 2 ++
backend/iam/sso/saml/views.py | 5 +++-
docker-compose-build.sh | 1 +
docker-compose-build.yml | 12 +++-----
frontend/src/lib/allauth.ts | 4 +--
frontend/src/lib/utils/constants.ts | 6 ++++
generate-caddyfile.sh | 46 +++++++++++++++++++++++++++++
start-caddy.sh | 17 -----------
10 files changed, 66 insertions(+), 28 deletions(-)
rename Caddyfile => Caddyfile.example (100%)
create mode 100755 generate-caddyfile.sh
delete mode 100755 start-caddy.sh
diff --git a/.gitignore b/.gitignore
index fbd7893f6..9de4da5ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ db/
.dccache
/backend/profiles
./backend/ciso_assistant/.meta
+Caddyfile
diff --git a/Caddyfile b/Caddyfile.example
similarity index 100%
rename from Caddyfile
rename to Caddyfile.example
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index dd6a632dd..9548df261 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -349,6 +349,8 @@ def set_ciso_assistant_url(_, __, event_dict):
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "email"
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
ACCOUNT_ADAPTER = "iam.adapter.MyAccountAdapter"
SOCIALACCOUNT_ADAPTER = "iam.adapter.MySocialAccountAdapter"
diff --git a/backend/iam/sso/saml/views.py b/backend/iam/sso/saml/views.py
index 6d52b56c0..b626174af 100644
--- a/backend/iam/sso/saml/views.py
+++ b/backend/iam/sso/saml/views.py
@@ -47,7 +47,10 @@ class FinishACSView(SAMLViewMixin, View):
def dispatch(self, request, organization_slug):
if len(SSOSettings.objects.all()) == 0:
raise Http404()
- provider = self.get_provider(organization_slug)
+ try:
+ provider = self.get_provider(organization_slug)
+ except:
+ return render_authentication_error(request, None)
acs_session = LoginSession(request, "saml_acs_session", "saml-acs-session")
acs_request = None
acs_request_data = acs_session.store.get("request")
diff --git a/docker-compose-build.sh b/docker-compose-build.sh
index ed5eed373..531b91074 100755
--- a/docker-compose-build.sh
+++ b/docker-compose-build.sh
@@ -17,6 +17,7 @@ if [ -f db/ciso-assistant.sqlite3 ]; then
echo "You should launch 'docker compose up -d'."
else
prepare_meta_file
+ bash generate-caddyfile.sh
# Build and start the containers
docker compose -f docker-compose-build.yml build
diff --git a/docker-compose-build.yml b/docker-compose-build.yml
index a8996c777..1b184df4a 100644
--- a/docker-compose-build.yml
+++ b/docker-compose-build.yml
@@ -8,7 +8,7 @@ services:
dockerfile: Dockerfile
restart: always
environment:
- - ALLOWED_HOSTS=backend
+ - ALLOWED_HOSTS=backend,localhost
- CISO_ASSISTANT_URL=https://localhost:8443
- DJANGO_DEBUG=True
volumes:
@@ -18,6 +18,7 @@ services:
container_name: frontend
environment:
- PUBLIC_BACKEND_API_URL=http://backend:8000/api
+ - PUBLIC_BACKEND_API_EXPOSED_URL=https://localhost:9443/api
- PROTOCOL_HEADER=x-forwarded-proto
- HOST_HEADER=x-forwarded-host
@@ -31,12 +32,7 @@ services:
restart: unless-stopped
ports:
- 8443:8443
- command:
- - caddy
- - reverse-proxy
- - --from
- - https://localhost:8443
- - --to
- - frontend:3000
+ - 9443:9443
volumes:
- ./db:/data
+ - ./Caddyfile:/etc/caddy/Caddyfile
diff --git a/frontend/src/lib/allauth.ts b/frontend/src/lib/allauth.ts
index 99d0d05c7..2f832c59b 100644
--- a/frontend/src/lib/allauth.ts
+++ b/frontend/src/lib/allauth.ts
@@ -1,7 +1,7 @@
import { getCSRFToken } from './django.js';
-import { BASE_API_URL } from '$lib/utils/constants';
+import { BACKEND_API_EXPOSED_URL } from '$lib/utils/constants';
-const BASE_URL = `${BASE_API_URL}`;
+const BASE_URL = `${BACKEND_API_EXPOSED_URL}`;
export const AuthProcess = Object.freeze({
LOGIN: 'login',
diff --git a/frontend/src/lib/utils/constants.ts b/frontend/src/lib/utils/constants.ts
index afe68ab81..a1ada0d8c 100644
--- a/frontend/src/lib/utils/constants.ts
+++ b/frontend/src/lib/utils/constants.ts
@@ -6,6 +6,12 @@ export const BASE_API_URL = `${
: 'http://localhost:8000/api'
}`;
+export const BACKEND_API_EXPOSED_URL = `${
+ env.hasOwnProperty('PUBLIC_BACKEND_API_EXPOSED_URL')
+ ? env.PUBLIC_BACKEND_API_EXPOSED_URL
+ : BASE_API_URL
+}`;
+
export const RISK_COLOR_PALETTE: string[] = ['#BBF7D0', '#BEF264', '#FEF08A', '#FBBF24', '#F87171'];
export const COMPLIANCE_COLOR_MAP = {
in_progress: '#3b82f6',
diff --git a/generate-caddyfile.sh b/generate-caddyfile.sh
new file mode 100755
index 000000000..ebb9f5d00
--- /dev/null
+++ b/generate-caddyfile.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+prompt_for_input() {
+ local var_name="$1"
+ local prompt_message="$2"
+ local default_value="$3"
+ local input_value
+
+ if [[ -z "${!var_name}" ]]; then
+ read -p "$prompt_message (press Enter to use default $default_value): " input_value
+ input_value="${input_value:-$default_value}"
+ export "$var_name"="$input_value"
+ fi
+}
+
+DEFAULT_FRONTEND_URL="https://localhost:8443"
+DEFAULT_BACKEND_URL="https://localhost:9443"
+
+prompt_for_input "CISO_FRONTEND_URL" "Enter the frontend URL" "$DEFAULT_FRONTEND_URL"
+prompt_for_input "CISO_BACKEND_URL" "Enter the backend URL" "$DEFAULT_BACKEND_URL"
+
+extract_domain() {
+ local url="$1"
+ if [[ "$url" =~ https://([^/]+) ]]; then
+ echo "${BASH_REMATCH[1]}"
+ else
+ echo "Invalid URL: $url"
+ exit 1
+ fi
+}
+
+frontend_target=$(extract_domain "$CISO_FRONTEND_URL")
+backend_target=$(extract_domain "$CISO_BACKEND_URL")
+
+caddyfile_content="${frontend_target} {
+ reverse_proxy frontend:3000
+}
+
+${backend_target} {
+ reverse_proxy /api/iam/sso/redirect backend:8000
+ reverse_proxy /api/accounts/saml/0/acs/ backend:8000
+ reverse_proxy /api/accounts/saml/0/acs/finish/ backend:8000
+}
+"
+
+echo "$caddyfile_content" > Caddyfile
diff --git a/start-caddy.sh b/start-caddy.sh
deleted file mode 100755
index 6074692e9..000000000
--- a/start-caddy.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-# create Caddyfile and start caddy
-
-python -c "
-import os, re, pathlib
-url = os.environ.get('CISO_ASSISTANT_URL')
-assert url, 'missing environment variable CISO_ASSISTANT_URL'
-q=re.match(r'https://([^/]+)', url)
-assert q, 'bad environment variable CISO_ASSISTANT_URL'
-target=q.group(1)
-a = target + ''' {
- reverse_proxy /accounts/saml/acs/ localhost:8000
- reverse_proxy /accounts/saml/acs/finish/ localhost:8000
- reverse_proxy /* localhost:3000
-}'''
-pathlib.Path('Caddyfile').write_text(a)
-" && caddy run
From 4a2f5800cc1b25dd630afe3a7b1ebf68ee1112b4 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 16:21:28 +0200
Subject: [PATCH 066/115] feat: update docker compose script
---
docker-compose.sh | 1 +
docker-compose.yml | 12 ++++--------
generate-caddyfile.sh | 5 +++++
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/docker-compose.sh b/docker-compose.sh
index 209e3ae91..7f61aacbf 100755
--- a/docker-compose.sh
+++ b/docker-compose.sh
@@ -5,6 +5,7 @@ if [ -f db/ciso-assistant.sqlite3 ] ; then
echo "you should launch docker compose up -d"
echo "for clean start, you can remove the database file, run docker compose down and then docker compose rm and start again"
else
+ bash generate-caddyfile.sh
docker rmi ghcr.io/intuitem/ciso-assistant-community/backend:latest ghcr.io/intuitem/ciso-assistant-community/frontend:latest 2> /dev/null
docker compose up -d
echo "Giving sometime for the database to be ready, please wait ..."
diff --git a/docker-compose.yml b/docker-compose.yml
index 15e660402..9a6bdca05 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,7 +6,7 @@ services:
image: ghcr.io/intuitem/ciso-assistant-community/backend:latest
restart: always
environment:
- - ALLOWED_HOSTS=backend
+ - ALLOWED_HOSTS=backend,localhost
- CISO_ASSISTANT_URL=https://localhost:8443
- DJANGO_DEBUG=True
- AUTH_TOKEN_TTL=7200
@@ -17,6 +17,7 @@ services:
container_name: frontend
environment:
- PUBLIC_BACKEND_API_URL=http://backend:8000/api
+ - PUBLIC_BACKEND_API_EXPOSED_URL=https://localhost:9443/api
- PROTOCOL_HEADER=x-forwarded-proto
- HOST_HEADER=x-forwarded-host
@@ -32,12 +33,7 @@ services:
restart: unless-stopped
ports:
- 8443:8443
- command:
- - caddy
- - reverse-proxy
- - --from
- - https://localhost:8443
- - --to
- - frontend:3000
+ - 9443:9443
volumes:
- ./caddy_data:/data
+ - ./Caddyfile:/etc/caddy/Caddyfile
diff --git a/generate-caddyfile.sh b/generate-caddyfile.sh
index ebb9f5d00..ce6bc8607 100755
--- a/generate-caddyfile.sh
+++ b/generate-caddyfile.sh
@@ -1,5 +1,10 @@
#!/bin/bash
+if [[ -f "Caddyfile" ]]; then
+ echo "Caddyfile already exists. Please remove it before generating a new one."
+ exit 0
+fi
+
prompt_for_input() {
local var_name="$1"
local prompt_message="$2"
From a2bc3e66502d475fda6fb5be0dc83aaa393f7ca8 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 16:24:17 +0200
Subject: [PATCH 067/115] chore: run format
---
backend/ciso_assistant/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index 9548df261..fa1c9e0bf 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -349,7 +349,7 @@ def set_ciso_assistant_url(_, __, event_dict):
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "email"
-SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
ACCOUNT_ADAPTER = "iam.adapter.MyAccountAdapter"
SOCIALACCOUNT_ADAPTER = "iam.adapter.MySocialAccountAdapter"
From ac52f4f2b8839c14fda5b53a4c3f44f4e4df416f Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 17:48:35 +0200
Subject: [PATCH 068/115] feat: track Caddyfile
---
.gitignore | 1 -
Caddyfile | 12 ++++++++++++
Caddyfile.example | 10 ----------
3 files changed, 12 insertions(+), 11 deletions(-)
create mode 100644 Caddyfile
delete mode 100644 Caddyfile.example
diff --git a/.gitignore b/.gitignore
index 9de4da5ec..fbd7893f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,3 @@ db/
.dccache
/backend/profiles
./backend/ciso_assistant/.meta
-Caddyfile
diff --git a/Caddyfile b/Caddyfile
new file mode 100644
index 000000000..26da8f22b
--- /dev/null
+++ b/Caddyfile
@@ -0,0 +1,12 @@
+# Basic Caddyfile for local deployment with SAML endpoints exposed, you can adapt it to your needs.
+
+localhost:8443 {
+ reverse_proxy frontend:3000
+}
+
+localhost:9443 {
+ reverse_proxy /api/iam/sso/redirect backend:8000
+ reverse_proxy /api/accounts/saml/0/acs/ backend:8000
+ reverse_proxy /api/accounts/saml/0/acs/finish/ backend:8000
+}
+
diff --git a/Caddyfile.example b/Caddyfile.example
deleted file mode 100644
index 25974f390..000000000
--- a/Caddyfile.example
+++ /dev/null
@@ -1,10 +0,0 @@
-# u24.proxmox.lan is an example for a remote host, adjust to your case when using this variant
-u24.proxmox.lan:8443 {
- reverse_proxy frontend:3000
- tls internal
-}
-
-u24.proxmox.lan:9443 {
- reverse_proxy backend:8000
- tls internal
-}
From c9775a6eecade11924163524e57747c0af274843 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 17:57:00 +0200
Subject: [PATCH 069/115] chore: cleaning
---
backend/iam/sso/serializers.py | 76 -------------------
backend/iam/views.py | 1 -
.../lib/components/Modals/CreateModal.svelte | 2 +-
.../[id=uuid]/edit/+page.svelte | 1 -
4 files changed, 1 insertion(+), 79 deletions(-)
diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py
index 89eace686..348b5d814 100644
--- a/backend/iam/sso/serializers.py
+++ b/backend/iam/sso/serializers.py
@@ -190,79 +190,3 @@ def update(self, instance, validated_data):
settings_object.value = validated_data
settings_object.save()
return instance
-
- # def build_saml_settings(self, validated_data):
- # default_attribute_mapping = SAMLProvider.default_attribute_mapping
- # attribute_mapping = {
- # "uid": validated_data.pop("attribute_mapping_uid", None),
- # "email_verified": validated_data.pop(
- # "attribute_mapping_email_verified", None
- # ),
- # "email": validated_data.pop("attribute_mapping_email", None),
- # }
- # return {
- # "attribute_mapping": {
- # key: value if value is not None else default_attribute_mapping[key]
- # for key, value in attribute_mapping.items()
- # },
- # "idp": {
- # "entity_id": validated_data.pop("idp_entity_id", ""),
- # "metadata_url": validated_data.pop("metadata_url", ""),
- # "sso_url": validated_data.pop("sso_url", ""),
- # "slo_url": validated_data.pop("slo_url", ""),
- # "x509cert": validated_data.pop("x509cert", ""),
- # },
- # "sp": {
- # # Optional entity ID of the SP. If not set, defaults to the `saml_metadata` urlpattern
- # "entity_id": validated_data.pop("sp_entity_id", "ciso-assistant"),
- # },
- # # Advanced settings.
- # "advanced": {
- # "allow_repeat_attribute_name": validated_data.pop(
- # "allow_repeat_attribute_name", True
- # ),
- # "allow_single_label_domains": validated_data.pop(
- # "allow_single_label_domains", False
- # ),
- # "authn_request_signed": validated_data.pop(
- # "authn_request_signed", False
- # ),
- # "digest_algorithm": validated_data.pop(
- # "digest_algorithm",
- # "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- # ),
- # "logout_request_signed": validated_data.pop(
- # "logout_request_signed", False
- # ),
- # "logout_response_signed": validated_data.pop(
- # "logout_response_signed", False
- # ),
- # "metadata_signed": validated_data.pop("metadata_signed", False),
- # "name_id_encrypted": validated_data.pop("name_id_encrypted", False),
- # "reject_deprecated_algorithm": validated_data.pop(
- # "reject_deprecated_algorithm", True
- # ),
- # # Due to security concerns, IdP initiated SSO is rejected by default.
- # "reject_idp_initiated_sso": validated_data.pop(
- # "reject_idp_initiated_sso", False
- # ),
- # "signature_algorithm": validated_data.pop(
- # "signature_algorithm",
- # "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
- # ),
- # "want_assertion_encrypted": validated_data.pop(
- # "want_assertion_encrypted", False
- # ),
- # "want_assertion_signed": validated_data.pop(
- # "want_assertion_signed", False
- # ),
- # "want_attribute_statement": validated_data.pop(
- # "want_attribute_statement", True
- # ),
- # "want_message_signed": validated_data.pop("want_message_signed", False),
- # "want_name_id": validated_data.pop("want_name_id", False),
- # "want_name_id_encrypted": validated_data.pop(
- # "want_name_id_encrypted", False
- # ),
- # },
- # }
diff --git a/backend/iam/views.py b/backend/iam/views.py
index 5f58d1576..464a21b8a 100644
--- a/backend/iam/views.py
+++ b/backend/iam/views.py
@@ -51,7 +51,6 @@ 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/frontend/src/lib/components/Modals/CreateModal.svelte b/frontend/src/lib/components/Modals/CreateModal.svelte
index bb3e73f33..8d7fe949a 100644
--- a/frontend/src/lib/components/Modals/CreateModal.svelte
+++ b/frontend/src/lib/components/Modals/CreateModal.svelte
@@ -42,6 +42,6 @@
-
+
{/if}
diff --git a/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.svelte b/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.svelte
index ba1bab756..aa078ffeb 100644
--- a/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.svelte
+++ b/frontend/src/routes/(app)/[model=urlmodel]/[id=uuid]/edit/+page.svelte
@@ -15,5 +15,4 @@
foreignKeys={data.foreignKeys}
model={data.model}
origin="edit"
- debug
/>
From 972fa69f596db1359be1a79c9bc0f150049d3d32 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 18:23:46 +0200
Subject: [PATCH 070/115] fix: models creation order
---
backend/ciso_assistant/settings.py | 2 +-
backend/core/apps.py | 47 +++++++++++++++++++++++++
backend/global_settings/apps.py | 56 ------------------------------
3 files changed, 48 insertions(+), 57 deletions(-)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index fa1c9e0bf..25049275e 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -122,8 +122,8 @@ def set_ciso_assistant_url(_, __, event_dict):
"django_structlog",
"tailwind",
"iam",
- "core",
"global_settings",
+ "core",
"cal",
"django_filters",
# "debug_toolbar",
diff --git a/backend/core/apps.py b/backend/core/apps.py
index cff098355..027f8e85f 100644
--- a/backend/core/apps.py
+++ b/backend/core/apps.py
@@ -257,7 +257,9 @@ def startup(sender: AppConfig, **kwargs):
Create superuser if CISO_ASSISTANT_SUPERUSER_EMAIL defined
"""
from django.contrib.auth.models import Permission
+ from allauth.socialaccount.providers.saml.provider import SAMLProvider
from iam.models import Folder, Role, RoleAssignment, User, UserGroup
+ from global_settings.models import GlobalSettings
print("startup handler: initialize database")
@@ -357,6 +359,51 @@ def startup(sender: AppConfig, **kwargs):
except Exception as e:
print(e) # NOTE: Add this exception in the logger
+ default_attribute_mapping = SAMLProvider.default_attribute_mapping
+
+ settings = {
+ "attribute_mapping": {
+ "uid": default_attribute_mapping["uid"],
+ "email_verified": default_attribute_mapping["email_verified"],
+ "email": default_attribute_mapping["email"],
+ },
+ "idp": {
+ "entity_id": "",
+ "metadata_url": "",
+ "sso_url": "",
+ "slo_url": "",
+ "x509cert": "",
+ },
+ "sp": {
+ "entity_id": "ciso-assistant",
+ },
+ "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,
+ "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,
+ },
+ }
+
+ if not GlobalSettings.objects.filter(name=GlobalSettings.Names.SSO).exists():
+ GlobalSettings.objects.get_or_create(
+ name=GlobalSettings.Names.SSO,
+ value={"client_id": "0", "settings": settings},
+ )
+
call_command("storelibraries")
diff --git a/backend/global_settings/apps.py b/backend/global_settings/apps.py
index ea4907f5d..0ac14adb2 100644
--- a/backend/global_settings/apps.py
+++ b/backend/global_settings/apps.py
@@ -2,62 +2,6 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
-
-def startup(sender: AppConfig, **kwargs):
- from .models import GlobalSettings
- from allauth.socialaccount.providers.saml.provider import SAMLProvider
-
- default_attribute_mapping = SAMLProvider.default_attribute_mapping
-
- settings = {
- "attribute_mapping": {
- "uid": default_attribute_mapping["uid"],
- "email_verified": default_attribute_mapping["email_verified"],
- "email": default_attribute_mapping["email"],
- },
- "idp": {
- "entity_id": "",
- "metadata_url": "",
- "sso_url": "",
- "slo_url": "",
- "x509cert": "",
- },
- "sp": {
- "entity_id": "ciso-assistant",
- },
- "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,
- "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,
- },
- }
-
- if not GlobalSettings.objects.filter(name=GlobalSettings.Names.SSO).exists():
- GlobalSettings.objects.get_or_create(
- name=GlobalSettings.Names.SSO,
- value={"client_id": "0", "settings": settings},
- )
-
-
class SettingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "global_settings"
-
- def ready(self):
- # avoid post_migrate handler if we are in the main, as it interferes with restore
- if not os.environ.get("RUN_MAIN"):
- post_migrate.connect(startup, sender=self)
From 83f6f10d5f2f832eea7d351c1a3f8fed36f99a1f Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 18:25:10 +0200
Subject: [PATCH 071/115] chore: run format
---
backend/core/apps.py | 2 +-
backend/global_settings/apps.py | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/backend/core/apps.py b/backend/core/apps.py
index 027f8e85f..e61361da6 100644
--- a/backend/core/apps.py
+++ b/backend/core/apps.py
@@ -360,7 +360,7 @@ def startup(sender: AppConfig, **kwargs):
print(e) # NOTE: Add this exception in the logger
default_attribute_mapping = SAMLProvider.default_attribute_mapping
-
+
settings = {
"attribute_mapping": {
"uid": default_attribute_mapping["uid"],
diff --git a/backend/global_settings/apps.py b/backend/global_settings/apps.py
index 0ac14adb2..a5690d44b 100644
--- a/backend/global_settings/apps.py
+++ b/backend/global_settings/apps.py
@@ -2,6 +2,7 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
+
class SettingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "global_settings"
From 1149f60c7b51fb3473a8133fef983dee75d5e3a5 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 20 Jun 2024 18:46:36 +0200
Subject: [PATCH 072/115] Default setLanguageTag result to english to avoid it
being undefined
---
frontend/src/hooks.server.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts
index d88bbeb6b..c09d5c41e 100644
--- a/frontend/src/hooks.server.ts
+++ b/frontend/src/hooks.server.ts
@@ -52,7 +52,7 @@ export const handle: Handle = async ({ event, resolve }) => {
const errorId = new URL(event.request.url).searchParams.get('error');
if (errorId) {
- setLanguageTag(event.cookies.get('ciso_lang'));
+ setLanguageTag(event.cookies.get('ciso_lang') || 'en');
setFlash({ type: 'error', message: m.failedSSO() }, event);
redirect(302, '/login');
}
From 14482a1fbb78f7bf373fa3d766d8f8a7941e0d8f Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Thu, 20 Jun 2024 18:48:44 +0200
Subject: [PATCH 073/115] chore: Merge migrations
---
.../migrations/0001_initial.py | 56 ++++--------------
.../0002_alter_globalsettings_value.py | 17 ------
backend/iam/migrations/0004_ssosettings.py | 59 ++++++-------------
3 files changed, 29 insertions(+), 103 deletions(-)
delete mode 100644 backend/global_settings/migrations/0002_alter_globalsettings_value.py
diff --git a/backend/global_settings/migrations/0001_initial.py b/backend/global_settings/migrations/0001_initial.py
index 182ab6dee..f50acda0f 100644
--- a/backend/global_settings/migrations/0001_initial.py
+++ b/backend/global_settings/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.4 on 2024-06-18 10:47
+# Generated by Django 5.0.6 on 2024-06-20 16:48
import django.db.models.deletion
import iam.models
@@ -7,59 +7,27 @@
class Migration(migrations.Migration):
+
initial = True
dependencies = [
- ("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"),
+ ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
]
operations = [
migrations.CreateModel(
- name="GlobalSettings",
+ name='GlobalSettings',
fields=[
- (
- "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"),
- ),
- (
- "name",
- models.CharField(
- choices=[("general", "General"), ("sso", "SSO")],
- default="general",
- max_length=30,
- unique=True,
- ),
- ),
- ("value", models.JSONField()),
- (
- "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",
- ),
- ),
+ ('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')),
+ ('name', models.CharField(choices=[('general', 'General'), ('sso', 'SSO')], default='general', max_length=30, unique=True)),
+ ('value', models.JSONField(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')),
],
options={
- "abstract": False,
+ 'abstract': False,
},
),
]
diff --git a/backend/global_settings/migrations/0002_alter_globalsettings_value.py b/backend/global_settings/migrations/0002_alter_globalsettings_value.py
deleted file mode 100644
index 15b296eda..000000000
--- a/backend/global_settings/migrations/0002_alter_globalsettings_value.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 5.0.4 on 2024-06-18 12:42
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("global_settings", "0001_initial"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="globalsettings",
- name="value",
- field=models.JSONField(default=dict),
- ),
- ]
diff --git a/backend/iam/migrations/0004_ssosettings.py b/backend/iam/migrations/0004_ssosettings.py
index aa6e3d6eb..44d700171 100644
--- a/backend/iam/migrations/0004_ssosettings.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -1,58 +1,33 @@
-# Generated by Django 5.0.4 on 2024-06-18 10:47
+# Generated by Django 5.0.6 on 2024-06-20 16:48
+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"),
- ("global_settings", "0001_initial"),
+ ('global_settings', '0001_initial'),
+ ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
]
operations = [
migrations.CreateModel(
- name="SSOSettings",
+ name='SSOSettings',
fields=[
- ("provider", models.CharField(max_length=30, verbose_name="provider")),
- (
- "provider_id",
- models.CharField(
- blank=True, max_length=200, verbose_name="provider ID"
- ),
- ),
- (
- "provider_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)),
+ ('globalsettings_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='global_settings.globalsettings')),
+ ('is_enabled', models.BooleanField(default=False, verbose_name='is enabled')),
+ ('provider', models.CharField(max_length=30, verbose_name='provider')),
+ ('provider_id', models.CharField(blank=True, max_length=200, verbose_name='provider ID')),
+ ('provider_name', models.CharField(max_length=200, verbose_name='name')),
+ ('client_id', models.CharField(default='0', 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)),
],
options={
- "managed": False,
- "proxy": True,
+ 'managed': False,
},
- bases=("global_settings.globalsettings",),
+ bases=('global_settings.globalsettings',),
),
]
From 1a9e298e1cc15b76bdcff3741126c1973afd0569 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 19:06:42 +0200
Subject: [PATCH 074/115] fix: try to fix ci
---
frontend/messages/en.json | 1 -
frontend/src/lib/components/SideBar/navData.ts | 2 +-
frontend/src/routes/(app)/settings/+page.server.ts | 8 --------
frontend/src/routes/(app)/settings/+page.svelte | 1 +
4 files changed, 2 insertions(+), 10 deletions(-)
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index edabb9815..1c0dc2784 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -571,7 +571,6 @@
"identityProvider": "Identity provider",
"identityProviders": "Identity providers",
"addIdentityProvider": "Add identity provider",
- "globalSettings": "Global settings",
"clientIDHelpText": "App ID, or consumer key",
"secretHelpText": "API secret, client secret, or consumer secret",
"SAMLIdPConfiguration": "SAML IdP configuration",
diff --git a/frontend/src/lib/components/SideBar/navData.ts b/frontend/src/lib/components/SideBar/navData.ts
index 95ff15160..ac5d868db 100644
--- a/frontend/src/lib/components/SideBar/navData.ts
+++ b/frontend/src/lib/components/SideBar/navData.ts
@@ -180,7 +180,7 @@ export const navData = {
permissions: ['backup']
},
{
- name: 'globalSettings',
+ name: 'settings',
fa_icon: 'fa-solid fa-cog',
href: '/settings',
permissions: ['change_globalsettings']
diff --git a/frontend/src/routes/(app)/settings/+page.server.ts b/frontend/src/routes/(app)/settings/+page.server.ts
index bbedc7aad..deb8125fb 100644
--- a/frontend/src/routes/(app)/settings/+page.server.ts
+++ b/frontend/src/routes/(app)/settings/+page.server.ts
@@ -1,20 +1,12 @@
import { BASE_API_URL } from '$lib/utils/constants';
import {
getModelInfo,
- urlParamModelForeignKeyFields,
- urlParamModelSelectFields,
- urlParamModelVerboseName
} from '$lib/utils/crud';
-import { localItems, toCamelCase } from '$lib/utils/locales';
import { IdentityProviderSchema, modelSchema } from '$lib/utils/schemas';
-import type { ModelInfo } from '$lib/utils/types';
-import * as m from '$paraglide/messages';
-import { languageTag } from '$paraglide/runtime';
import { fail, type Actions } from '@sveltejs/kit';
import { setFlash } from 'sveltekit-flash-message/server';
import { setError, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
-import { z } from 'zod';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, fetch }) => {
diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte
index 04a723ebc..7d43e1b0a 100644
--- a/frontend/src/routes/(app)/settings/+page.svelte
+++ b/frontend/src/routes/(app)/settings/+page.svelte
@@ -3,6 +3,7 @@
import { IdentityProviderSchema } from '$lib/utils/schemas';
export let data;
+
From ce324d09c06bf6c99c1978ac7ca6093bc09c12e4 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 19:08:51 +0200
Subject: [PATCH 075/115] chore: run format
---
.../migrations/0001_initial.py | 54 +++++++++++---
backend/iam/migrations/0004_ssosettings.py | 71 +++++++++++++++----
.../src/routes/(app)/settings/+page.server.ts | 4 +-
.../src/routes/(app)/settings/+page.svelte | 1 -
4 files changed, 100 insertions(+), 30 deletions(-)
diff --git a/backend/global_settings/migrations/0001_initial.py b/backend/global_settings/migrations/0001_initial.py
index f50acda0f..cdcfcb797 100644
--- a/backend/global_settings/migrations/0001_initial.py
+++ b/backend/global_settings/migrations/0001_initial.py
@@ -7,27 +7,59 @@
class Migration(migrations.Migration):
-
initial = True
dependencies = [
- ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
+ ("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"),
]
operations = [
migrations.CreateModel(
- name='GlobalSettings',
+ name="GlobalSettings",
fields=[
- ('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')),
- ('name', models.CharField(choices=[('general', 'General'), ('sso', 'SSO')], default='general', max_length=30, unique=True)),
- ('value', models.JSONField(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')),
+ (
+ "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"),
+ ),
+ (
+ "name",
+ models.CharField(
+ choices=[("general", "General"), ("sso", "SSO")],
+ default="general",
+ max_length=30,
+ unique=True,
+ ),
+ ),
+ ("value", models.JSONField(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",
+ ),
+ ),
],
options={
- 'abstract': False,
+ "abstract": False,
},
),
]
diff --git a/backend/iam/migrations/0004_ssosettings.py b/backend/iam/migrations/0004_ssosettings.py
index 44d700171..26b524f2d 100644
--- a/backend/iam/migrations/0004_ssosettings.py
+++ b/backend/iam/migrations/0004_ssosettings.py
@@ -5,29 +5,70 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('global_settings', '0001_initial'),
- ('iam', '0003_alter_folder_updated_at_alter_role_updated_at_and_more'),
+ ("global_settings", "0001_initial"),
+ ("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"),
]
operations = [
migrations.CreateModel(
- name='SSOSettings',
+ name="SSOSettings",
fields=[
- ('globalsettings_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='global_settings.globalsettings')),
- ('is_enabled', models.BooleanField(default=False, verbose_name='is enabled')),
- ('provider', models.CharField(max_length=30, verbose_name='provider')),
- ('provider_id', models.CharField(blank=True, max_length=200, verbose_name='provider ID')),
- ('provider_name', models.CharField(max_length=200, verbose_name='name')),
- ('client_id', models.CharField(default='0', 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)),
+ (
+ "globalsettings_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="global_settings.globalsettings",
+ ),
+ ),
+ (
+ "is_enabled",
+ models.BooleanField(default=False, verbose_name="is enabled"),
+ ),
+ ("provider", models.CharField(max_length=30, verbose_name="provider")),
+ (
+ "provider_id",
+ models.CharField(
+ blank=True, max_length=200, verbose_name="provider ID"
+ ),
+ ),
+ (
+ "provider_name",
+ models.CharField(max_length=200, verbose_name="name"),
+ ),
+ (
+ "client_id",
+ models.CharField(
+ default="0",
+ 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)),
],
options={
- 'managed': False,
+ "managed": False,
},
- bases=('global_settings.globalsettings',),
+ bases=("global_settings.globalsettings",),
),
]
diff --git a/frontend/src/routes/(app)/settings/+page.server.ts b/frontend/src/routes/(app)/settings/+page.server.ts
index deb8125fb..84d48a050 100644
--- a/frontend/src/routes/(app)/settings/+page.server.ts
+++ b/frontend/src/routes/(app)/settings/+page.server.ts
@@ -1,7 +1,5 @@
import { BASE_API_URL } from '$lib/utils/constants';
-import {
- getModelInfo,
-} from '$lib/utils/crud';
+import { getModelInfo } from '$lib/utils/crud';
import { IdentityProviderSchema, modelSchema } from '$lib/utils/schemas';
import { fail, type Actions } from '@sveltejs/kit';
import { setFlash } from 'sveltekit-flash-message/server';
diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte
index 7d43e1b0a..04a723ebc 100644
--- a/frontend/src/routes/(app)/settings/+page.svelte
+++ b/frontend/src/routes/(app)/settings/+page.svelte
@@ -3,7 +3,6 @@
import { IdentityProviderSchema } from '$lib/utils/schemas';
export let data;
-
From 94eeb3d729f1a62eee81d66c14f6880658616237 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 19:50:03 +0200
Subject: [PATCH 076/115] fix: add page proxy settings for ci
---
frontend/src/lib/utils/table.ts | 5 ++++-
frontend/src/routes/(app)/settings/+page.svelte | 8 --------
.../src/routes/(app)/settings/{ => sso}/+page.server.ts | 0
frontend/src/routes/(app)/settings/sso/+page.svelte | 8 ++++++++
4 files changed, 12 insertions(+), 9 deletions(-)
rename frontend/src/routes/(app)/settings/{ => sso}/+page.server.ts (100%)
create mode 100644 frontend/src/routes/(app)/settings/sso/+page.svelte
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index 3e0e385cf..dc1ad0813 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -108,5 +108,8 @@ export const listViewFields = {
'identity-providers': {
head: ['name', 'provider', 'providerId'],
body: ['name', 'provider', 'provider_id']
- }
+ },
+ 'settings': {
+ breadcrumb_link_disabled: false
+ },
};
diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte
index 04a723ebc..e69de29bb 100644
--- a/frontend/src/routes/(app)/settings/+page.svelte
+++ b/frontend/src/routes/(app)/settings/+page.svelte
@@ -1,8 +0,0 @@
-
-
-
diff --git a/frontend/src/routes/(app)/settings/+page.server.ts b/frontend/src/routes/(app)/settings/sso/+page.server.ts
similarity index 100%
rename from frontend/src/routes/(app)/settings/+page.server.ts
rename to frontend/src/routes/(app)/settings/sso/+page.server.ts
diff --git a/frontend/src/routes/(app)/settings/sso/+page.svelte b/frontend/src/routes/(app)/settings/sso/+page.svelte
new file mode 100644
index 000000000..04a723ebc
--- /dev/null
+++ b/frontend/src/routes/(app)/settings/sso/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
From c322c9b85509c831e68fbb0c9582bc5c63babaa1 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Thu, 20 Jun 2024 20:24:59 +0200
Subject: [PATCH 077/115] Revert "fix: add page proxy settings for ci"
---
frontend/src/lib/utils/table.ts | 5 +----
.../src/routes/(app)/settings/{sso => }/+page.server.ts | 0
frontend/src/routes/(app)/settings/+page.svelte | 8 ++++++++
frontend/src/routes/(app)/settings/sso/+page.svelte | 8 --------
4 files changed, 9 insertions(+), 12 deletions(-)
rename frontend/src/routes/(app)/settings/{sso => }/+page.server.ts (100%)
delete mode 100644 frontend/src/routes/(app)/settings/sso/+page.svelte
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index dc1ad0813..3e0e385cf 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -108,8 +108,5 @@ export const listViewFields = {
'identity-providers': {
head: ['name', 'provider', 'providerId'],
body: ['name', 'provider', 'provider_id']
- },
- 'settings': {
- breadcrumb_link_disabled: false
- },
+ }
};
diff --git a/frontend/src/routes/(app)/settings/sso/+page.server.ts b/frontend/src/routes/(app)/settings/+page.server.ts
similarity index 100%
rename from frontend/src/routes/(app)/settings/sso/+page.server.ts
rename to frontend/src/routes/(app)/settings/+page.server.ts
diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte
index e69de29bb..04a723ebc 100644
--- a/frontend/src/routes/(app)/settings/+page.svelte
+++ b/frontend/src/routes/(app)/settings/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
diff --git a/frontend/src/routes/(app)/settings/sso/+page.svelte b/frontend/src/routes/(app)/settings/sso/+page.svelte
deleted file mode 100644
index 04a723ebc..000000000
--- a/frontend/src/routes/(app)/settings/sso/+page.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
From 8e2e9375172495ad1b862a01980fbefd9110d288 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 21 Jun 2024 09:22:59 +0200
Subject: [PATCH 078/115] chore: Remove dead code
---
backend/iam/sso/urls.py | 12 +++---------
1 file changed, 3 insertions(+), 9 deletions(-)
diff --git a/backend/iam/sso/urls.py b/backend/iam/sso/urls.py
index 01c7acd18..d60acb156 100644
--- a/backend/iam/sso/urls.py
+++ b/backend/iam/sso/urls.py
@@ -1,13 +1,7 @@
-from django.urls import include, path
-from rest_framework import routers
-
-from .views import SSOSettingsViewSet, RedirectToProviderView
-
-
-router = routers.DefaultRouter()
+from django.urls import path
+from .views import RedirectToProviderView
urlpatterns = [
- path("", include(router.urls)),
- path("redirect", RedirectToProviderView.as_view(), name="sso-redirect"),
+ path("redirect/", RedirectToProviderView.as_view(), name="sso-redirect"),
]
From 1a99f48ca7470bbb6515dc841facbcb7426167ff Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 21 Jun 2024 09:23:14 +0200
Subject: [PATCH 079/115] Add trailing slash to provider redirect URL
---
frontend/src/lib/allauth.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/lib/allauth.ts b/frontend/src/lib/allauth.ts
index 2f832c59b..bca2cf5bf 100644
--- a/frontend/src/lib/allauth.ts
+++ b/frontend/src/lib/allauth.ts
@@ -9,7 +9,7 @@ export const AuthProcess = Object.freeze({
});
export const URLs = Object.freeze({
- REDIRECT_TO_PROVIDER: BASE_URL + '/iam/sso/redirect'
+ REDIRECT_TO_PROVIDER: BASE_URL + '/iam/sso/redirect/'
});
function postForm(action, data) {
From 539e4ac06bebee68d97306e9ee5b71136b4ef934 Mon Sep 17 00:00:00 2001
From: Nassim Tabchiche
Date: Fri, 21 Jun 2024 09:23:42 +0200
Subject: [PATCH 080/115] Re-enable CSRF middleware
---
backend/ciso_assistant/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py
index 25049275e..e1e118bc7 100644
--- a/backend/ciso_assistant/settings.py
+++ b/backend/ciso_assistant/settings.py
@@ -144,7 +144,7 @@ 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",
From 2b23d98f460cdaff1a23ed0e325517bfc687d9d1 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Fri, 21 Jun 2024 09:36:04 +0200
Subject: [PATCH 081/115] fix: add trailing slash to provider redirect URL in
Caddyfile
---
Caddyfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Caddyfile b/Caddyfile
index 26da8f22b..26154230d 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -5,7 +5,7 @@ localhost:8443 {
}
localhost:9443 {
- reverse_proxy /api/iam/sso/redirect backend:8000
+ reverse_proxy /api/iam/sso/redirect/ backend:8000
reverse_proxy /api/accounts/saml/0/acs/ backend:8000
reverse_proxy /api/accounts/saml/0/acs/finish/ backend:8000
}
From 872949bb3e1826af2cf4d045e3e76f9ca3555aa2 Mon Sep 17 00:00:00 2001
From: Mohamed-Hacene
Date: Fri, 21 Jun 2024 13:32:30 +0200
Subject: [PATCH 082/115] style: improve settings UI
---
frontend/messages/en.json | 5 ++++-
.../src/lib/components/Forms/ModelForm.svelte | 21 ++++++++++--------
.../src/routes/(app)/settings/+layout.svelte | 3 ---
.../src/routes/(app)/settings/+page.server.ts | 2 ++
.../src/routes/(app)/settings/+page.svelte | 9 +++++++-
.../(app)/settings/SettingsLayout.svelte | 22 +++++++++++++++++++
6 files changed, 48 insertions(+), 14 deletions(-)
delete mode 100644 frontend/src/routes/(app)/settings/+layout.svelte
create mode 100644 frontend/src/routes/(app)/settings/SettingsLayout.svelte
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 1c0dc2784..f5088d935 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -608,5 +608,8 @@
"loginSSO": "Login with SSO",
"or": "or",
"errorImportingLibrary": "Error during library import",
- "libraryImportError": "An error occurred during library import"
+ "libraryImportError": "An error occurred during library import",
+ "ssoSettingsupdated": "SSO settings updated",
+ "ssoSettings": "SSO settings",
+ "ssoSettingsDescription": "Configure your Single Sign-On settings here."
}
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 0f241e036..50fa646e7 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -29,6 +29,7 @@
export let closeModal = false;
export let parent: any;
export let suggestions: { [key: string]: any } = {};
+ export let cancelButton = true;
const URLModel = model.urlModel as urlModel;
export let schema = modelSchema(URLModel);
@@ -457,7 +458,7 @@
{/if}
{#if data.provider === 'saml'}
- {m.SAMLIdPConfiguration()}
+ {m.SAMLIdPConfiguration()}
- {m.SPConfiguration()}
+ {m.SPConfiguration()}
{m.advancedSettings()}
+ >{m.advancedSettings()}
{m.save()}
{:else}
-
+ {#if cancelButton}
+
+ {/if}