diff --git a/README.md b/README.md index 19ce6802d..10010b0f3 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,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/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py index 1a552fc3d..b12724739 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 """ @@ -122,6 +122,7 @@ def set_ciso_assistant_url(_, __, event_dict): "django_structlog", "tailwind", "iam", + "global_settings", "core", "cal", "django_filters", @@ -131,6 +132,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 = [ @@ -143,13 +149,13 @@ def set_ciso_assistant_url(_, __, event_dict): "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django_structlog.middlewares.RequestMiddleware", - # "debug_toolbar.middleware.DebugToolbarMiddleware", - # "pyinstrument.middleware.ProfilerMiddleware", + "allauth.account.middleware.AccountMiddleware", ] ROOT_URLCONF = "ciso_assistant.urls" -LOGIN_REDIRECT_URL = "home" -LOGOUT_REDIRECT_URL = "login" +# we leave these for the API UI tools - even if Django templates and Admin are not used anymore +LOGIN_REDIRECT_URL = "/api" +LOGOUT_REDIRECT_URL = "/api" AUTH_TOKEN_TTL = int( os.environ.get("AUTH_TOKEN_TTL", default=60 * 60) @@ -201,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" @@ -333,3 +342,31 @@ 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" + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +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": CISO_ASSISTANT_URL + "/login", +} + +SOCIALACCOUNT_PROVIDERS = { + "saml": { + "EMAIL_AUTHENTICATION": True, + "VERIFIED_EMAIL": True, + }, +} diff --git a/backend/core/apps.py b/backend/core/apps.py index b90f7359c..faae6b48b 100644 --- a/backend/core/apps.py +++ b/backend/core/apps.py @@ -4,6 +4,9 @@ import os from django.core.management import call_command +from structlog import get_logger + +logger = get_logger(__name__) READER_PERMISSIONS_LIST = [ "view_project", @@ -245,6 +248,8 @@ "delete_loadedlibrary", "backup", "restore", + "view_globalsettings", + "change_globalsettings", ] @@ -255,7 +260,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") @@ -355,6 +362,53 @@ 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(): + logger.info("SSO settings not found, creating default settings") + sso_settings = GlobalSettings.objects.create( + name=GlobalSettings.Names.SSO, + value={"client_id": "0", "settings": settings}, + ) + logger.info("SSO settings created", settings=sso_settings.value) + call_command("storelibraries") 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/serializers.py b/backend/core/serializers.py index 4dcdb80ca..094a107e7 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -274,6 +274,7 @@ class Meta: "is_active", "date_joined", "user_groups", + "is_sso", ] diff --git a/backend/core/urls.py b/backend/core/urls.py index 2f8062ee6..2854fa616 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -1,5 +1,7 @@ +from iam.sso.views import SSOSettingsViewSet from .views import * from library.views import StoredLibraryViewSet, LoadedLibraryViewSet +from iam.sso.saml.views import FinishACSView from django.urls import include, path @@ -42,11 +44,11 @@ router.register(r"stored-libraries", StoredLibraryViewSet, basename="stored-libraries") router.register(r"loaded-libraries", LoadedLibraryViewSet, basename="loaded-libraries") - urlpatterns = [ path("", include(router.urls)), path("iam/", include("iam.urls")), path("serdes/", include("serdes.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"), @@ -55,6 +57,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")), ] if DEBUG: diff --git a/backend/core/views.py b/backend/core/views.py index 6a545fad6..117b7b361 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 @@ -91,7 +93,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 @@ -135,7 +137,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/global_settings/__init__.py b/backend/global_settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/global_settings/apps.py b/backend/global_settings/apps.py new file mode 100644 index 000000000..a5690d44b --- /dev/null +++ b/backend/global_settings/apps.py @@ -0,0 +1,8 @@ +import os +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" diff --git a/backend/global_settings/migrations/0001_initial.py b/backend/global_settings/migrations/0001_initial.py new file mode 100644 index 000000000..cdcfcb797 --- /dev/null +++ b/backend/global_settings/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# Generated by Django 5.0.6 on 2024-06-20 16:48 + +import django.db.models.deletion +import iam.models +import uuid +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=[ + ( + "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, + }, + ), + ] diff --git a/backend/global_settings/migrations/__init__.py b/backend/global_settings/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/global_settings/models.py b/backend/global_settings/models.py new file mode 100644 index 000000000..5d9e8c323 --- /dev/null +++ b/backend/global_settings/models.py @@ -0,0 +1,28 @@ +from django.db import models + +from iam.models import FolderMixin +from core.base_models import AbstractBaseModel + + +class GlobalSettings(AbstractBaseModel, FolderMixin): + """ + 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, + choices=Names, + default=Names.GENERAL, + ) + # Value of the setting. + value = models.JSONField(default=dict) + + def __str__(self): + return self.name diff --git a/backend/global_settings/routers.py b/backend/global_settings/routers.py new file mode 100644 index 000000000..d2e2be69d --- /dev/null +++ b/backend/global_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/global_settings/serializers.py b/backend/global_settings/serializers.py new file mode 100644 index 000000000..1e6e8c547 --- /dev/null +++ b/backend/global_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/global_settings/urls.py b/backend/global_settings/urls.py new file mode 100644 index 000000000..73aab14c9 --- /dev/null +++ b/backend/global_settings/urls.py @@ -0,0 +1,25 @@ +from django.urls import include, path +from rest_framework import routers + +from iam.sso.views import SSOSettingsViewSet + +from .views import GlobalSettingsViewSet, get_sso_info +from .routers import DefaultSettingsRouter + + +router = routers.DefaultRouter() +router.register(r"global", GlobalSettingsViewSet, basename="global-settings") + +settings_router = DefaultSettingsRouter() +settings_router.register( + r"sso", + SSOSettingsViewSet, + basename="sso-settings", +) + + +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/global_settings/views.py b/backend/global_settings/views.py new file mode 100644 index 000000000..fbb172c6c --- /dev/null +++ b/backend/global_settings/views.py @@ -0,0 +1,51 @@ +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 + +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, + ) + + +@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/backend/iam/adapter.py b/backend/iam/adapter.py new file mode 100644 index 000000000..0cb062241 --- /dev/null +++ b/backend/iam/adapter.py @@ -0,0 +1,127 @@ +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, + MultipleObjectsReturned, + warnings, +) +from django.db.models import Q +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() + + +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): + 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 + ) + + def list_apps(self, request, provider=None, client_id=None): + """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 SSOSettings + + # Map provider to the list of apps. + provider_to_apps = {} + + # First, populate it with the DB backed apps. + db_apps = SSOSettings.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 = SSOSettings(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 SSOSettings + + 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 SSOSettings.DoesNotExist() + return apps[0] diff --git a/backend/iam/migrations/0004_ssosettings_user_is_sso.py b/backend/iam/migrations/0004_ssosettings_user_is_sso.py new file mode 100644 index 000000000..a916b089d --- /dev/null +++ b/backend/iam/migrations/0004_ssosettings_user_is_sso.py @@ -0,0 +1,79 @@ +# Generated by Django 5.0.4 on 2024-06-21 22:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("global_settings", "0001_initial"), + ("iam", "0003_alter_folder_updated_at_alter_role_updated_at_and_more"), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + options={ + "managed": False, + }, + bases=("global_settings.globalsettings",), + ), + migrations.AddField( + model_name="user", + name="is_sso", + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/iam/models.py b/backend/iam/models.py index 4ffa9880e..ce52e80c0 100644 --- a/backend/iam/models.py +++ b/backend/iam/models.py @@ -286,6 +286,7 @@ class User(AbstractBaseUser, AbstractBaseModel, FolderMixin): first_name = models.CharField(_("first name"), max_length=150, blank=True) email = models.CharField(max_length=100, unique=True) first_login = models.BooleanField(default=True) + is_sso = models.BooleanField(default=False) is_active = models.BooleanField( _("active"), default=True, 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/models.py b/backend/iam/sso/models.py new file mode 100644 index 000000000..c63d73713 --- /dev/null +++ b/backend/iam/sso/models.py @@ -0,0 +1,110 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ObjectDoesNotExist +from django.db.models.query import QuerySet + +from allauth.socialaccount.models import providers +from global_settings.models import GlobalSettings + + +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( + id=_settings.id, + name=_settings.name, + 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"), + provider_name=_settings.value.get("name"), + 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() + + is_enabled = models.BooleanField( + verbose_name=_("is enabled"), + default=False, + ) + + provider = models.CharField( + verbose_name=_("provider"), + max_length=30, + ) + provider_id = models.CharField( + verbose_name=_("provider ID"), + max_length=200, + blank=True, + ) + provider_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"), + default="0", + ) + 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: + managed = False + + def get_name(self): + return GlobalSettings.Names.SSO.label + + def __str__(self): + return self.get_name() + + def save(self, *args, **kwargs): + raise NotImplementedError("SSOSettings is read-only.") + + def get_provider_display(self): + _providers = {p[0]: p[1] for p in providers.registry.as_choices()} + return _providers.get(self.provider) + + def get_provider(self, request): + provider_class = providers.registry.get_class(self.provider) + return provider_class(request=request, app=self) 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..60cc53396 --- /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"^(?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..2527d673a --- /dev/null +++ b/backend/iam/sso/saml/views.py @@ -0,0 +1,140 @@ +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.http.response import Http404 +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View +from rest_framework.views import csrf_exempt + +import structlog + +from iam.models import User +from iam.sso.models import SSOSettings +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): + if len(SSOSettings.objects.all()) == 0: + raise Http404() + try: + provider = self.get_provider(organization_slug) + except: + logger.error("Could not get provider") + 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") + 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 + try: + email = auth._nameid + user = User.objects.get(email=email) + idp_first_name = auth._attributes.get( + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" + )[0] + idp_last_name = auth._attributes.get( + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" + )[0] + if user.first_name != idp_first_name: + user.first_name = idp_first_name + if user.last_name != idp_last_name: + user.last_name = idp_last_name + user.is_sso = True + user.save() + token = generate_token(user) + login.state["next"] += f"sso/authenticate/{token}" + return complete_social_login(request, login) + except User.DoesNotExist: + logger.warning("User does not exist") + return render_authentication_error( + request, provider, error="UserDoesNotExist" + ) + except: + return render_authentication_error(request, provider, error="failedSSO") diff --git a/backend/iam/sso/serializers.py b/backend/iam/sso/serializers.py new file mode 100644 index 000000000..348b5d814 --- /dev/null +++ b/backend/iam/sso/serializers.py @@ -0,0 +1,192 @@ +from allauth.socialaccount.providers.saml.provider import SAMLProvider +from rest_framework import serializers + +from global_settings.models import GlobalSettings +from .models import SSOSettings + +from core.serializers import BaseModelSerializer + + +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) + + class Meta: + model = SSOSettings + exclude = ["value"] + + +class SSOSettingsWriteSerializer(BaseModelSerializer): + is_enabled = serializers.BooleanField( + required=False, + ) + 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, + ), + 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, + ), + 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, + ), + required=False, + allow_null=True, + source="settings.attribute_mapping.email", + ) + idp_entity_id = serializers.CharField( + 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, + source="settings.idp.metadata_url", + ) + sso_url = serializers.CharField( + 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, + source="settings.idp.slo_url", + ) + x509cert = serializers.CharField( + 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, + source="settings.sp.entity_id", + ) + allow_repeat_attribute_name = serializers.BooleanField( + required=False, + source="settings.advanced.allow_repeat_attribute_name", + ) + allow_single_label_domains = serializers.BooleanField( + required=False, + source="settings.advanced.allow_single_label_domains", + ) + authn_request_signed = serializers.BooleanField( + required=False, + source="settings.advanced.authn_request_signed", + ) + digest_algorithm = serializers.CharField( + 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", + ) + reject_deprecated_algorithm = serializers.BooleanField( + required=False, + source="settings.advanced.reject_deprecated_algorithm", + ) + reject_idp_initiated_sso = serializers.BooleanField( + required=False, + source="settings.advanced.reject_idp_initiated_sso", + ) + signature_algorithm = serializers.CharField( + 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 + exclude = ["value"] + + def update(self, instance, validated_data): + settings_object = GlobalSettings.objects.get(name=GlobalSettings.Names.SSO) + settings_object.value = validated_data + settings_object.save() + return instance diff --git a/backend/iam/sso/urls.py b/backend/iam/sso/urls.py new file mode 100644 index 000000000..d60acb156 --- /dev/null +++ b/backend/iam/sso/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import RedirectToProviderView + +urlpatterns = [ + path("redirect/", RedirectToProviderView.as_view(), name="sso-redirect"), +] diff --git a/backend/iam/sso/views.py b/backend/iam/sso/views.py new file mode 100644 index 000000000..b43f30100 --- /dev/null +++ b/backend/iam/sso/views.py @@ -0,0 +1,70 @@ +from rest_framework.response import Response +from core.views import BaseModelViewSet as AbstractBaseModelViewSet +from .models import SSOSettings +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 +from structlog import get_logger + +logger = get_logger(__name__) + + +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: + logger.error("Cannot perform redirection, Check your IdP URLs") + return render_authentication_error(request, provider, error="failedSSO") + + +class BaseModelViewSet(AbstractBaseModelViewSet): + serializers_module = "iam.sso.serializers" + + +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=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) 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")), ] 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/backend/requirements.txt b/backend/requirements.txt index 232319cca..2d1495209 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,4 +18,7 @@ structlog==24.2.0 python-dotenv==1.0.1 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 diff --git a/backend/serdes/views.py b/backend/serdes/views.py index 4cd2cb218..926d5ebcc 100644 --- a/backend/serdes/views.py +++ b/backend/serdes/views.py @@ -30,7 +30,13 @@ def get(self, request, *args, **kwargs): # NOTE: We will not be able to dump selected folders with this method. management.call_command( dumpdata.Command(), - exclude=["contenttypes", "auth.permission", "sessions.session"], + exclude=[ + "contenttypes", + "auth.permission", + "sessions.session", + "iam.ssosettings", + "knox.authtoken", + ], indent=4, stdout=response, natural_foreign=True, @@ -56,7 +62,12 @@ def post(self, request, *args, **kwargs): "-", format="json", verbosity=0, - exclude=["contenttypes", "auth.permission", "sessions.session"], + exclude=[ + "contenttypes", + "auth.permission", + "sessions.session", + "knox.authtoken", + ], ) return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/docker-compose-build.yml b/docker-compose-build.yml index a8996c777..328f476f1 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:8443/api - PROTOCOL_HEADER=x-forwarded-proto - HOST_HEADER=x-forwarded-host @@ -28,15 +29,17 @@ services: caddy: container_name: caddy image: caddy:2.7.6 + environment: + - CISO_ASSISTANT_URL=https://localhost:8443 restart: unless-stopped ports: - 8443:8443 - command: - - caddy - - reverse-proxy - - --from - - https://localhost:8443 - - --to - - frontend:3000 volumes: - ./db:/data + command: | + sh -c 'echo $$CISO_ASSISTANT_URL "{ + 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 + reverse_proxy /* frontend:3000 + }" > Caddyfile && caddy run' diff --git a/docker-compose.yml b/docker-compose.yml index 15e660402..c27326b99 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:8443/api - PROTOCOL_HEADER=x-forwarded-proto - HOST_HEADER=x-forwarded-host @@ -27,17 +28,19 @@ services: caddy: container_name: caddy image: caddy:2.7.6 + environment: + - CISO_ASSISTANT_URL=https://localhost:8443 depends_on: - frontend restart: unless-stopped ports: - 8443:8443 - command: - - caddy - - reverse-proxy - - --from - - https://localhost:8443 - - --to - - frontend:3000 volumes: - ./caddy_data:/data + command: | + sh -c 'echo $CISO_ASSISTANT_URL "{ + 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 + reverse_proxy /* frontend:3000 + }" > Caddyfile && caddy run' diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 61edca38a..2e9317e2b 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", @@ -475,7 +475,13 @@ "attachmentDeleted": "Der Anhang wurde erfolgreich gelöscht", "librarySuccessfullyLoaded": "Die Bibliothek wurde erfolgreich geladen", "noLibraryDetected": "Keine Bibliothek erkannt", - "errorImportingLibrary": "Fehler beim Importieren der Bibliothek", + "errorLoadingLibrary": "Fehler beim Laden der Bibliothek", + "updateThisLibrary": "Aktualisieren Sie diese Bibliothek", + "librarySuccessfullyUpdated": "Bibliothek erfolgreich aktualisiert", + "libraryNotFound": "Bibliothek nicht gefunden", + "libraryHasNoUpdate": "Diese Bibliothek hat kein Update", + "dependencyNotFound": "Abhängigkeit nicht gefunden", + "invalidLibraryUpdate": "Ungültiges Bibliotheksupdate", "passwordSuccessfullyChanged": "Ihr Passwort wurde erfolgreich geändert", "passwordSuccessfullyReset": "Ihr Passwort wurde erfolgreich zurückgesetzt", "passwordSuccessfullySet": "Ihr Passwort wurde erfolgreich festgelegt", @@ -499,7 +505,7 @@ "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.", + "libraryLoadingError": "Beim Laden 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.", @@ -555,5 +561,16 @@ "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.", + "settings": "Einstellungen", + "advancedSettings": "Erweiterte Einstellungen", + "enableSSO": "Aktivieren von SSO", + "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.", + "ssoSettingsupdated": "SSO-Einstellungen aktualisiert", + "ssoSettings": "SSO-Einstellungen", + "ssoSettingsDescription": "Konfigurieren Sie hier Ihre Single Sign-On-Einstellungen." } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 3ad1e74f7..d998426fc 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", @@ -562,5 +562,56 @@ "appliedControlNoReferenceControl": "Applied control has no reference control selected", "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." + "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", + "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", + "failedSSO": "SSO authentication failed, please contact your administrator", + "UserDoesNotExist": "User not declared, please contact your administrator", + "loginSSO": "Login with SSO", + "or": "or", + "errorImportingLibrary": "Error 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.", + "sso": "SSO", + "isSso": "Is SSO" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 4fc26f3e3..911a72402 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", @@ -475,7 +475,13 @@ "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", + "errorLoadingLibrary": "Error al cargar la biblioteca", + "updateThisLibrary": "Actualizar esta biblioteca", + "librarySuccessfullyUpdated": "Biblioteca actualizada con éxito", + "libraryNotFound": "Biblioteca no encontrada", + "libraryHasNoUpdate": "Esta biblioteca no tiene actualización.", + "dependencyNotFound": "Dependencia no encontrada", + "invalidLibraryUpdate": "Actualización de biblioteca no válida", "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", @@ -499,7 +505,7 @@ "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.", + "libraryLoadingError": "Se produjo un error durante la carga 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.", @@ -530,6 +536,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", @@ -551,9 +560,17 @@ "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.", + "settings": "Ajustes", + "advancedSettings": "Ajustes avanzados", + "enableSSO": "Habilitar SSO", + "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.", + "ssoSettingsupdated": "Configuración de SSO actualizada", + "ssoSettings": "Configuración de inicio de sesión único", + "ssoSettingsDescription": "Configure sus ajustes de inicio de sesión único aquí." } diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index c09c5258e..bbe6a8c07 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", @@ -561,5 +561,16 @@ "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.", + "settings": "Paramètres", + "advancedSettings": "Réglages avancés", + "enableSSO": "Activer le SSO", + "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", + "ssoSettingsupdated": "Paramètres SSO mis à jour", + "ssoSettings": "Paramètres SSO", + "ssoSettingsDescription": "Configurez vos paramètres d'authentification unique ici." } diff --git a/frontend/messages/it.json b/frontend/messages/it.json index 902e082aa..07585312c 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", @@ -475,7 +475,13 @@ "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", + "errorLoadingLibrary": "Errore durante il caricamento della libreria", + "updateThisLibrary": "Aggiorna questa libreria", + "librarySuccessfullyUpdated": "Libreria aggiornata con successo", + "libraryNotFound": "Libreria non trovata", + "libraryHasNoUpdate": "Questa libreria non ha aggiornamenti", + "dependencyNotFound": "Dipendenza non trovata", + "invalidLibraryUpdate": "Aggiornamento della libreria non valido", "passwordSuccessfullyChanged": "La tua password è stata cambiata con successo", "passwordSuccessfullyReset": "La tua password è stata reimpostata con successo", "passwordSuccessfullySet": "La tua password è stata impostata con successo", @@ -499,7 +505,7 @@ "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.", + "libraryLoadingError": "Si è verificato un errore durante il caricamento della libreria", "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.", @@ -555,5 +561,16 @@ "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.", + "settings": "Impostazioni", + "advancedSettings": "Impostazioni avanzate", + "enableSSO": "Abilita SSO", + "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.", + "ssoSettingsupdated": "Impostazioni SSO aggiornate", + "ssoSettings": "Impostazioni SSO", + "ssoSettingsDescription": "Configura qui le tue impostazioni Single Sign-On." } diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index f95e632a4..25b81b885 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", @@ -475,7 +475,13 @@ "attachmentDeleted": "De bijlage is succesvol verwijderd", "librarySuccessfullyLoaded": "De bibliotheek is succesvol geladen", "noLibraryDetected": "Geen bibliotheek gedetecteerd", - "errorImportingLibrary": "Fout bij het importeren van de bibliotheek", + "errorLoadingLibrary": "Fout bij het laden van bibliotheek", + "updateThisLibrary": "Update deze bibliotheek", + "librarySuccessfullyUpdated": "Bibliotheek succesvol bijgewerkt", + "libraryNotFound": "Bibliotheek niet gevonden", + "libraryHasNoUpdate": "Deze bibliotheek heeft geen update", + "dependencyNotFound": "Afhankelijkheid niet gevonden", + "invalidLibraryUpdate": "Ongeldige bibliotheekupdate", "passwordSuccessfullyChanged": "Je wachtwoord is succesvol gewijzigd", "passwordSuccessfullyReset": "Je wachtwoord is succesvol gereset", "passwordSuccessfullySet": "Je wachtwoord is succesvol ingesteld", @@ -499,7 +505,7 @@ "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.", + "libraryLoadingError": "Er is een fout opgetreden tijdens het laden van uw 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.", @@ -555,5 +561,16 @@ "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.", + "settings": "Instellingen", + "advancedSettings": "Geavanceerde instellingen", + "enableSSO": "SSO inschakelen", + "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.", + "ssoSettingsupdated": "SSO-instellingen bijgewerkt", + "ssoSettings": "SSO-instellingen", + "ssoSettingsDescription": "Configureer hier uw Single Sign-On-instellingen." } 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/messages/pt.json b/frontend/messages/pt.json index 30f7acf83..8aff26c50 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -561,5 +561,16 @@ "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.", + "settings": "Configurações", + "advancedSettings": "Configurações avançadas", + "enableSSO": "Habilitar SSO", + "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", + "ssoSettingsupdated": "Configurações de SSO atualizadas", + "ssoSettings": "Configurações de SSO", + "ssoSettingsDescription": "Defina suas configurações de logon único aqui." } diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index a9380d8f0..fa59e477c 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,6 +1,9 @@ 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'; +import { setLanguageTag } from '$paraglide/runtime'; async function ensureCsrfToken(event: RequestEvent): Promise { let csrfToken = event.cookies.get('csrftoken') || ''; @@ -47,6 +50,13 @@ 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) { + setLanguageTag(event.cookies.get('ciso_lang') || 'en'); + setFlash({ type: 'error', message: m[errorId]() }, event); + redirect(302, '/login'); + } + const user = await validateUserSession(event); if (user) { event.locals.user = user; diff --git a/frontend/src/lib/allauth.ts b/frontend/src/lib/allauth.ts new file mode 100644 index 000000000..bca2cf5bf --- /dev/null +++ b/frontend/src/lib/allauth.ts @@ -0,0 +1,38 @@ +import { getCSRFToken } from './django.js'; +import { BACKEND_API_EXPOSED_URL } from '$lib/utils/constants'; + +const BASE_URL = `${BACKEND_API_EXPOSED_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/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/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 @@
-
+
{#if label !== undefined} {#if $constraints?.required}
diff --git a/frontend/src/lib/components/Forms/Form.svelte b/frontend/src/lib/components/Forms/Form.svelte index a255c0ff0..0d7682566 100644 --- a/frontend/src/lib/components/Forms/Form.svelte +++ b/frontend/src/lib/components/Forms/Form.svelte @@ -42,11 +42,12 @@ taintedMessage: taintedMessage }); - const { form, message /*, tainted*/, delayed, errors, allErrors, enhance } = _form; + const { form, message, tainted, delayed, errors, allErrors, enhance } = _form; {#if debug} + {/if} @@ -67,5 +68,6 @@ errors={$errors} allErrors={$allErrors} delayed={$delayed} + tainted={$tainted} /> diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index e9b80346b..318dc6166 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; @@ -28,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); @@ -425,6 +427,250 @@ {#if shape.is_active} {/if} + {:else if URLModel === 'sso-settings'} + + + + {#if data.provider !== 'saml'} + + {m.IdPConfiguration()} + + + + + {/if} + {#if data.provider === 'saml'} + + {m.SAMLIdPConfiguration()} + + + +