Compare commits
	
		
			6 Commits
		
	
	
		
			version/20
			...
			policies/p
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b3883f7fbf | |||
| 87c6b0128a | |||
| b243c97916 | |||
| 3f66527521 | |||
| 2f7c258657 | |||
| 917c90374f | 
| @ -1,16 +1,16 @@ | ||||
| [bumpversion] | ||||
| current_version = 2025.2.0-rc3 | ||||
| current_version = 2024.12.3 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||
| serialize =  | ||||
| serialize = | ||||
| 	{major}.{minor}.{patch}-{rc_t}{rc_n} | ||||
| 	{major}.{minor}.{patch} | ||||
| message = release: {new_version} | ||||
| tag_name = version/{new_version} | ||||
|  | ||||
| [bumpversion:part:rc_t] | ||||
| values =  | ||||
| values = | ||||
| 	rc | ||||
| 	final | ||||
| optional_value = final | ||||
|  | ||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @ -21,7 +21,7 @@ pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null) | ||||
| CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \ | ||||
| 		-I .github/codespell-words.txt \ | ||||
| 		-S 'web/src/locales/**' \ | ||||
| 		-S 'website/docs/developer-docs/api/reference/**' \ | ||||
| 		-S 'website/developer-docs/api/reference/**' \ | ||||
| 		-S '**/node_modules/**' \ | ||||
| 		-S '**/dist/**' \ | ||||
| 		$(PY_SOURCES) \ | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from os import environ | ||||
|  | ||||
| __version__ = "2025.2.0" | ||||
| __version__ = "2024.12.3" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -50,6 +50,7 @@ from authentik.enterprise.providers.microsoft_entra.models import ( | ||||
|     MicrosoftEntraProviderGroup, | ||||
|     MicrosoftEntraProviderUser, | ||||
| ) | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken | ||||
| from authentik.enterprise.providers.ssf.models import StreamEvent | ||||
| from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( | ||||
|     EndpointDevice, | ||||
| @ -71,7 +72,6 @@ from authentik.providers.oauth2.models import ( | ||||
|     DeviceToken, | ||||
|     RefreshToken, | ||||
| ) | ||||
| from authentik.providers.rac.models import ConnectionToken | ||||
| from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser | ||||
| from authentik.rbac.models import Role | ||||
| from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser | ||||
|  | ||||
| @ -4,7 +4,6 @@ from json import loads | ||||
|  | ||||
| from django.db.models import Prefetch | ||||
| from django.http import Http404 | ||||
| from django.utils.translation import gettext as _ | ||||
| from django_filters.filters import CharFilter, ModelMultipleChoiceFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| from drf_spectacular.utils import ( | ||||
| @ -82,37 +81,9 @@ class GroupSerializer(ModelSerializer): | ||||
|         if not self.instance or not parent: | ||||
|             return parent | ||||
|         if str(parent.group_uuid) == str(self.instance.group_uuid): | ||||
|             raise ValidationError(_("Cannot set group as parent of itself.")) | ||||
|             raise ValidationError("Cannot set group as parent of itself.") | ||||
|         return parent | ||||
|  | ||||
|     def validate_is_superuser(self, superuser: bool): | ||||
|         """Ensure that the user creating this group has permissions to set the superuser flag""" | ||||
|         request: Request = self.context.get("request", None) | ||||
|         if not request: | ||||
|             return superuser | ||||
|         # If we're updating an instance, and the state hasn't changed, we don't need to check perms | ||||
|         if self.instance and superuser == self.instance.is_superuser: | ||||
|             return superuser | ||||
|         user: User = request.user | ||||
|         perm = ( | ||||
|             "authentik_core.enable_group_superuser" | ||||
|             if superuser | ||||
|             else "authentik_core.disable_group_superuser" | ||||
|         ) | ||||
|         has_perm = user.has_perm(perm) | ||||
|         if self.instance and not has_perm: | ||||
|             has_perm = user.has_perm(perm, self.instance) | ||||
|         if not has_perm: | ||||
|             raise ValidationError( | ||||
|                 _( | ||||
|                     ( | ||||
|                         "User does not have permission to set " | ||||
|                         "superuser status to {superuser_status}." | ||||
|                     ).format_map({"superuser_status": superuser}) | ||||
|                 ) | ||||
|             ) | ||||
|         return superuser | ||||
|  | ||||
|     class Meta: | ||||
|         model = Group | ||||
|         fields = [ | ||||
|  | ||||
| @ -1,26 +0,0 @@ | ||||
| # Generated by Django 5.0.11 on 2025-01-30 23:55 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name="group", | ||||
|             options={ | ||||
|                 "permissions": [ | ||||
|                     ("add_user_to_group", "Add user to group"), | ||||
|                     ("remove_user_from_group", "Remove user from group"), | ||||
|                     ("enable_group_superuser", "Enable superuser status"), | ||||
|                     ("disable_group_superuser", "Disable superuser status"), | ||||
|                 ], | ||||
|                 "verbose_name": "Group", | ||||
|                 "verbose_name_plural": "Groups", | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -204,8 +204,6 @@ class Group(SerializerModel, AttributesMixin): | ||||
|         permissions = [ | ||||
|             ("add_user_to_group", _("Add user to group")), | ||||
|             ("remove_user_from_group", _("Remove user from group")), | ||||
|             ("enable_group_superuser", _("Enable superuser status")), | ||||
|             ("disable_group_superuser", _("Disable superuser status")), | ||||
|         ] | ||||
|  | ||||
|     def __str__(self): | ||||
|  | ||||
| @ -35,7 +35,8 @@ from authentik.flows.planner import ( | ||||
|     FlowPlanner, | ||||
| ) | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.denied import AccessDeniedResponse | ||||
| from authentik.policies.utils import delete_none_values | ||||
| @ -46,9 +47,8 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| PLAN_CONTEXT_SOURCE_GROUPS = "source_groups" | ||||
| SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages" | ||||
| SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token"  # nosec | ||||
| PLAN_CONTEXT_SOURCE_GROUPS = "source_groups" | ||||
|  | ||||
|  | ||||
| class MessageStage(StageView): | ||||
| @ -219,28 +219,28 @@ class SourceFlowManager: | ||||
|             } | ||||
|         ) | ||||
|         flow_context.update(self.policy_context) | ||||
|         if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session: | ||||
|             token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) | ||||
|             self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) | ||||
|             plan = token.plan | ||||
|             plan.context[PLAN_CONTEXT_IS_RESTORED] = token | ||||
|             plan.context.update(flow_context) | ||||
|             for stage in self.get_stages_to_append(flow): | ||||
|                 plan.append_stage(stage) | ||||
|             if stages: | ||||
|                 for stage in stages: | ||||
|                     plan.append_stage(stage) | ||||
|             self.request.session[SESSION_KEY_PLAN] = plan | ||||
|             flow_slug = token.flow.slug | ||||
|             token.delete() | ||||
|             return redirect_with_qs( | ||||
|                 "authentik_core:if-flow", | ||||
|                 self.request.GET, | ||||
|                 flow_slug=flow_slug, | ||||
|             ) | ||||
|         flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect) | ||||
|  | ||||
|         if not flow: | ||||
|             # We only check for the flow token here if we don't have a flow, otherwise we rely on | ||||
|             # SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add | ||||
|             # stages that deal with this token to return to another flow | ||||
|             if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session: | ||||
|                 token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) | ||||
|                 self._logger.info( | ||||
|                     "Replacing source flow with overridden flow", flow=token.flow.slug | ||||
|                 ) | ||||
|                 plan = token.plan | ||||
|                 plan.context[PLAN_CONTEXT_IS_RESTORED] = token | ||||
|                 plan.context.update(flow_context) | ||||
|                 for stage in self.get_stages_to_append(flow): | ||||
|                     plan.append_stage(stage) | ||||
|                 if stages: | ||||
|                     for stage in stages: | ||||
|                         plan.append_stage(stage) | ||||
|                 redirect = plan.to_redirect(self.request, token.flow) | ||||
|                 token.delete() | ||||
|                 return redirect | ||||
|             return bad_request_message( | ||||
|                 self.request, | ||||
|                 _("Configured flow does not exist."), | ||||
| @ -259,8 +259,6 @@ class SourceFlowManager: | ||||
|         if stages: | ||||
|             for stage in stages: | ||||
|                 plan.append_stage(stage) | ||||
|         for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []): | ||||
|             plan.append_stage(stage) | ||||
|         return plan.to_redirect(self.request, flow) | ||||
|  | ||||
|     def handle_auth( | ||||
| @ -297,8 +295,6 @@ class SourceFlowManager: | ||||
|         # When request isn't authenticated we jump straight to auth | ||||
|         if not self.request.user.is_authenticated: | ||||
|             return self.handle_auth(connection) | ||||
|         # When an override flow token exists we actually still use a flow for link | ||||
|         # to continue the existing flow we came from | ||||
|         if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session: | ||||
|             return self._prepare_flow(None, connection) | ||||
|         connection.save() | ||||
|  | ||||
| @ -67,8 +67,6 @@ def clean_expired_models(self: SystemTask): | ||||
|                 raise ImproperlyConfigured( | ||||
|                     "Invalid session_storage setting, allowed values are db and cache" | ||||
|                 ) | ||||
|     if CONFIG.get("session_storage", "cache") == "db": | ||||
|         DBSessionStore.clear_expired() | ||||
|     LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount) | ||||
|  | ||||
|     messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}") | ||||
|  | ||||
| @ -8,8 +8,6 @@ | ||||
|     <head> | ||||
|         <meta charset="UTF-8"> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||
|         {# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #} | ||||
|         <meta name="darkreader-lock"> | ||||
|         <title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title> | ||||
|         <link rel="icon" href="{{ brand.branding_favicon_url }}"> | ||||
|         <link rel="shortcut icon" href="{{ brand.branding_favicon_url }}"> | ||||
|  | ||||
| @ -4,7 +4,7 @@ from django.urls.base import reverse | ||||
| from guardian.shortcuts import assign_perm | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import Group | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_user | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
| @ -14,7 +14,7 @@ class TestGroupsAPI(APITestCase): | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.login_user = create_test_user() | ||||
|         self.user = create_test_user() | ||||
|         self.user = User.objects.create(username="test-user") | ||||
|  | ||||
|     def test_list_with_users(self): | ||||
|         """Test listing with users""" | ||||
| @ -109,57 +109,3 @@ class TestGroupsAPI(APITestCase): | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(res.status_code, 400) | ||||
|  | ||||
|     def test_superuser_no_perm(self): | ||||
|         """Test creating a superuser group without permission""" | ||||
|         assign_perm("authentik_core.add_group", self.login_user) | ||||
|         self.client.force_login(self.login_user) | ||||
|         res = self.client.post( | ||||
|             reverse("authentik_api:group-list"), | ||||
|             data={"name": generate_id(), "is_superuser": True}, | ||||
|         ) | ||||
|         self.assertEqual(res.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             res.content, | ||||
|             {"is_superuser": ["User does not have permission to set superuser status to True."]}, | ||||
|         ) | ||||
|  | ||||
|     def test_superuser_update_no_perm(self): | ||||
|         """Test updating a superuser group without permission""" | ||||
|         group = Group.objects.create(name=generate_id(), is_superuser=True) | ||||
|         assign_perm("view_group", self.login_user, group) | ||||
|         assign_perm("change_group", self.login_user, group) | ||||
|         self.client.force_login(self.login_user) | ||||
|         res = self.client.patch( | ||||
|             reverse("authentik_api:group-detail", kwargs={"pk": group.pk}), | ||||
|             data={"is_superuser": False}, | ||||
|         ) | ||||
|         self.assertEqual(res.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             res.content, | ||||
|             {"is_superuser": ["User does not have permission to set superuser status to False."]}, | ||||
|         ) | ||||
|  | ||||
|     def test_superuser_update_no_change(self): | ||||
|         """Test updating a superuser group without permission | ||||
|         and without changing the superuser status""" | ||||
|         group = Group.objects.create(name=generate_id(), is_superuser=True) | ||||
|         assign_perm("view_group", self.login_user, group) | ||||
|         assign_perm("change_group", self.login_user, group) | ||||
|         self.client.force_login(self.login_user) | ||||
|         res = self.client.patch( | ||||
|             reverse("authentik_api:group-detail", kwargs={"pk": group.pk}), | ||||
|             data={"name": generate_id(), "is_superuser": True}, | ||||
|         ) | ||||
|         self.assertEqual(res.status_code, 200) | ||||
|  | ||||
|     def test_superuser_create(self): | ||||
|         """Test creating a superuser group with permission""" | ||||
|         assign_perm("authentik_core.add_group", self.login_user) | ||||
|         assign_perm("authentik_core.enable_group_superuser", self.login_user) | ||||
|         self.client.force_login(self.login_user) | ||||
|         res = self.client.post( | ||||
|             reverse("authentik_api:group-list"), | ||||
|             data={"name": generate_id(), "is_superuser": True}, | ||||
|         ) | ||||
|         self.assertEqual(res.status_code, 201) | ||||
|  | ||||
| @ -97,8 +97,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | ||||
|         thread_kwargs: dict | None = None, | ||||
|         **_, | ||||
|     ): | ||||
|         if not self.enabled: | ||||
|             return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_) | ||||
|         if not should_log_model(instance): | ||||
|             return None | ||||
|         thread_kwargs = {} | ||||
| @ -124,8 +122,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | ||||
|     ): | ||||
|         thread_kwargs = {} | ||||
|         m2m_field = None | ||||
|         if not self.enabled: | ||||
|             return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs) | ||||
|         # For the audit log we don't care about `pre_` or `post_` so we trim that part off | ||||
|         _, _, action_direction = action.partition("_") | ||||
|         # resolve the "through" model to an actual field | ||||
|  | ||||
| @ -6,12 +6,13 @@ from rest_framework.viewsets import GenericViewSet | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import ModelSerializer | ||||
| from authentik.providers.rac.api.endpoints import EndpointSerializer | ||||
| from authentik.providers.rac.api.providers import RACProviderSerializer | ||||
| from authentik.providers.rac.models import ConnectionToken | ||||
| from authentik.enterprise.api import EnterpriseRequiredMixin | ||||
| from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer | ||||
| from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken | ||||
| 
 | ||||
| 
 | ||||
| class ConnectionTokenSerializer(ModelSerializer): | ||||
| class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer): | ||||
|     """ConnectionToken Serializer""" | ||||
| 
 | ||||
|     provider_obj = RACProviderSerializer(source="provider", read_only=True) | ||||
| @ -14,9 +14,10 @@ from structlog.stdlib import get_logger | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import ModelSerializer | ||||
| from authentik.core.models import Provider | ||||
| from authentik.enterprise.api import EnterpriseRequiredMixin | ||||
| from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer | ||||
| from authentik.enterprise.providers.rac.models import Endpoint | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.providers.rac.api.providers import RACProviderSerializer | ||||
| from authentik.providers.rac.models import Endpoint | ||||
| from authentik.rbac.filters import ObjectFilter | ||||
| 
 | ||||
| LOGGER = get_logger() | ||||
| @ -27,7 +28,7 @@ def user_endpoint_cache_key(user_pk: str) -> str: | ||||
|     return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}" | ||||
| 
 | ||||
| 
 | ||||
| class EndpointSerializer(ModelSerializer): | ||||
| class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer): | ||||
|     """Endpoint Serializer""" | ||||
| 
 | ||||
|     provider_obj = RACProviderSerializer(source="provider", read_only=True) | ||||
| @ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet | ||||
| from authentik.core.api.property_mappings import PropertyMappingSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import JSONDictField | ||||
| from authentik.providers.rac.models import RACPropertyMapping | ||||
| from authentik.enterprise.providers.rac.models import RACPropertyMapping | ||||
| 
 | ||||
| 
 | ||||
| class RACPropertyMappingSerializer(PropertyMappingSerializer): | ||||
| @ -5,10 +5,11 @@ from rest_framework.viewsets import ModelViewSet | ||||
| 
 | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.providers.rac.models import RACProvider | ||||
| from authentik.enterprise.api import EnterpriseRequiredMixin | ||||
| from authentik.enterprise.providers.rac.models import RACProvider | ||||
| 
 | ||||
| 
 | ||||
| class RACProviderSerializer(ProviderSerializer): | ||||
| class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer): | ||||
|     """RACProvider Serializer""" | ||||
| 
 | ||||
|     outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all") | ||||
							
								
								
									
										14
									
								
								authentik/enterprise/providers/rac/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/enterprise/providers/rac/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| """RAC app config""" | ||||
|  | ||||
| from authentik.enterprise.apps import EnterpriseConfig | ||||
|  | ||||
|  | ||||
| class AuthentikEnterpriseProviderRAC(EnterpriseConfig): | ||||
|     """authentik enterprise rac app config""" | ||||
|  | ||||
|     name = "authentik.enterprise.providers.rac" | ||||
|     label = "authentik_providers_rac" | ||||
|     verbose_name = "authentik Enterprise.Providers.RAC" | ||||
|     default = True | ||||
|     mountpoint = "" | ||||
|     ws_mountpoint = "authentik.enterprise.providers.rac.urls" | ||||
| @ -7,22 +7,22 @@ from channels.generic.websocket import AsyncWebsocketConsumer | ||||
| from django.http.request import QueryDict | ||||
| from structlog.stdlib import BoundLogger, get_logger | ||||
| 
 | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken, RACProvider | ||||
| from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE | ||||
| from authentik.outposts.models import Outpost, OutpostState, OutpostType | ||||
| from authentik.providers.rac.models import ConnectionToken, RACProvider | ||||
| 
 | ||||
| # Global broadcast group, which messages are sent to when the outpost connects back | ||||
| # to authentik for a specific connection | ||||
| # The `RACClientConsumer` consumer adds itself to this group on connection, | ||||
| # and removes itself once it has been assigned a specific outpost channel | ||||
| RAC_CLIENT_GROUP = "group_rac_client" | ||||
| RAC_CLIENT_GROUP = "group_enterprise_rac_client" | ||||
| # A group for all connections in a given authentik session ID | ||||
| # A disconnect message is sent to this group when the session expires/is deleted | ||||
| RAC_CLIENT_GROUP_SESSION = "group_rac_client_%(session)s" | ||||
| RAC_CLIENT_GROUP_SESSION = "group_enterprise_rac_client_%(session)s" | ||||
| # A group for all connections with a specific token, which in almost all cases | ||||
| # is just one connection, however this is used to disconnect the connection | ||||
| # when the token is deleted | ||||
| RAC_CLIENT_GROUP_TOKEN = "group_rac_token_%(token)s"  # nosec | ||||
| RAC_CLIENT_GROUP_TOKEN = "group_enterprise_rac_token_%(token)s"  # nosec | ||||
| 
 | ||||
| # Step 1: Client connects to this websocket endpoint | ||||
| # Step 2: We prepare all the connection args for Guac | ||||
| @ -3,7 +3,7 @@ | ||||
| from channels.exceptions import ChannelFull | ||||
| from channels.generic.websocket import AsyncWebsocketConsumer | ||||
| 
 | ||||
| from authentik.providers.rac.consumer_client import RAC_CLIENT_GROUP | ||||
| from authentik.enterprise.providers.rac.consumer_client import RAC_CLIENT_GROUP | ||||
| 
 | ||||
| 
 | ||||
| class RACOutpostConsumer(AsyncWebsocketConsumer): | ||||
| @ -74,7 +74,7 @@ class RACProvider(Provider): | ||||
| 
 | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.providers.rac.api.providers import RACProviderSerializer | ||||
|         from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer | ||||
| 
 | ||||
|         return RACProviderSerializer | ||||
| 
 | ||||
| @ -100,7 +100,7 @@ class Endpoint(SerializerModel, PolicyBindingModel): | ||||
| 
 | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.providers.rac.api.endpoints import EndpointSerializer | ||||
|         from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer | ||||
| 
 | ||||
|         return EndpointSerializer | ||||
| 
 | ||||
| @ -129,7 +129,7 @@ class RACPropertyMapping(PropertyMapping): | ||||
| 
 | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.providers.rac.api.property_mappings import ( | ||||
|         from authentik.enterprise.providers.rac.api.property_mappings import ( | ||||
|             RACPropertyMappingSerializer, | ||||
|         ) | ||||
| 
 | ||||
| @ -10,12 +10,12 @@ from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
| 
 | ||||
| from authentik.core.models import User | ||||
| from authentik.providers.rac.api.endpoints import user_endpoint_cache_key | ||||
| from authentik.providers.rac.consumer_client import ( | ||||
| from authentik.enterprise.providers.rac.api.endpoints import user_endpoint_cache_key | ||||
| from authentik.enterprise.providers.rac.consumer_client import ( | ||||
|     RAC_CLIENT_GROUP_SESSION, | ||||
|     RAC_CLIENT_GROUP_TOKEN, | ||||
| ) | ||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint | ||||
| 
 | ||||
| 
 | ||||
| @receiver(user_logged_out) | ||||
| @ -3,7 +3,7 @@ | ||||
| {% load authentik_core %} | ||||
| 
 | ||||
| {% block head %} | ||||
| <script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script> | ||||
| <script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script> | ||||
| <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)"> | ||||
| <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> | ||||
| <link rel="icon" href="{{ tenant.branding_favicon_url }}"> | ||||
| @ -1,9 +1,16 @@ | ||||
| """Test RAC Provider""" | ||||
| 
 | ||||
| from datetime import timedelta | ||||
| from time import mktime | ||||
| from unittest.mock import MagicMock, patch | ||||
| 
 | ||||
| from django.urls import reverse | ||||
| from django.utils.timezone import now | ||||
| from rest_framework.test import APITestCase | ||||
| 
 | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.enterprise.license import LicenseKey | ||||
| from authentik.enterprise.models import License | ||||
| from authentik.lib.generators import generate_id | ||||
| 
 | ||||
| 
 | ||||
| @ -13,8 +20,21 @@ class TestAPI(APITestCase): | ||||
|     def setUp(self) -> None: | ||||
|         self.user = create_test_admin_user() | ||||
| 
 | ||||
|     @patch( | ||||
|         "authentik.enterprise.license.LicenseKey.validate", | ||||
|         MagicMock( | ||||
|             return_value=LicenseKey( | ||||
|                 aud="", | ||||
|                 exp=int(mktime((now() + timedelta(days=3000)).timetuple())), | ||||
|                 name=generate_id(), | ||||
|                 internal_users=100, | ||||
|                 external_users=100, | ||||
|             ) | ||||
|         ), | ||||
|     ) | ||||
|     def test_create(self): | ||||
|         """Test creation of RAC Provider""" | ||||
|         License.objects.create(key=generate_id()) | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:racprovider-list"), | ||||
| @ -5,10 +5,10 @@ from rest_framework.test import APITestCase | ||||
| 
 | ||||
| from authentik.core.models import Application | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.providers.rac.models import Endpoint, Protocols, RACProvider | ||||
| 
 | ||||
| 
 | ||||
| class TestEndpointsAPI(APITestCase): | ||||
| @ -4,14 +4,14 @@ from django.test import TransactionTestCase | ||||
| 
 | ||||
| from authentik.core.models import Application, AuthenticatedSession | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.providers.rac.models import ( | ||||
| from authentik.enterprise.providers.rac.models import ( | ||||
|     ConnectionToken, | ||||
|     Endpoint, | ||||
|     Protocols, | ||||
|     RACPropertyMapping, | ||||
|     RACProvider, | ||||
| ) | ||||
| from authentik.lib.generators import generate_id | ||||
| 
 | ||||
| 
 | ||||
| class TestModels(TransactionTestCase): | ||||
| @ -1,17 +1,23 @@ | ||||
| """RAC Views tests""" | ||||
| 
 | ||||
| from datetime import timedelta | ||||
| from json import loads | ||||
| from time import mktime | ||||
| from unittest.mock import MagicMock, patch | ||||
| 
 | ||||
| from django.urls import reverse | ||||
| from django.utils.timezone import now | ||||
| from rest_framework.test import APITestCase | ||||
| 
 | ||||
| from authentik.core.models import Application | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.enterprise.license import LicenseKey | ||||
| from authentik.enterprise.models import License | ||||
| from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.policies.denied import AccessDeniedResponse | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.providers.rac.models import Endpoint, Protocols, RACProvider | ||||
| 
 | ||||
| 
 | ||||
| class TestRACViews(APITestCase): | ||||
| @ -33,8 +39,21 @@ class TestRACViews(APITestCase): | ||||
|             provider=self.provider, | ||||
|         ) | ||||
| 
 | ||||
|     @patch( | ||||
|         "authentik.enterprise.license.LicenseKey.validate", | ||||
|         MagicMock( | ||||
|             return_value=LicenseKey( | ||||
|                 aud="", | ||||
|                 exp=int(mktime((now() + timedelta(days=3000)).timetuple())), | ||||
|                 name=generate_id(), | ||||
|                 internal_users=100, | ||||
|                 external_users=100, | ||||
|             ) | ||||
|         ), | ||||
|     ) | ||||
|     def test_no_policy(self): | ||||
|         """Test request""" | ||||
|         License.objects.create(key=generate_id()) | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
| @ -51,6 +70,18 @@ class TestRACViews(APITestCase): | ||||
|         final_response = self.client.get(next_url) | ||||
|         self.assertEqual(final_response.status_code, 200) | ||||
| 
 | ||||
|     @patch( | ||||
|         "authentik.enterprise.license.LicenseKey.validate", | ||||
|         MagicMock( | ||||
|             return_value=LicenseKey( | ||||
|                 aud="", | ||||
|                 exp=int(mktime((now() + timedelta(days=3000)).timetuple())), | ||||
|                 name=generate_id(), | ||||
|                 internal_users=100, | ||||
|                 external_users=100, | ||||
|             ) | ||||
|         ), | ||||
|     ) | ||||
|     def test_app_deny(self): | ||||
|         """Test request (deny on app level)""" | ||||
|         PolicyBinding.objects.create( | ||||
| @ -58,6 +89,7 @@ class TestRACViews(APITestCase): | ||||
|             policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2), | ||||
|             order=0, | ||||
|         ) | ||||
|         License.objects.create(key=generate_id()) | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
| @ -67,6 +99,18 @@ class TestRACViews(APITestCase): | ||||
|         ) | ||||
|         self.assertIsInstance(response, AccessDeniedResponse) | ||||
| 
 | ||||
|     @patch( | ||||
|         "authentik.enterprise.license.LicenseKey.validate", | ||||
|         MagicMock( | ||||
|             return_value=LicenseKey( | ||||
|                 aud="", | ||||
|                 exp=int(mktime((now() + timedelta(days=3000)).timetuple())), | ||||
|                 name=generate_id(), | ||||
|                 internal_users=100, | ||||
|                 external_users=100, | ||||
|             ) | ||||
|         ), | ||||
|     ) | ||||
|     def test_endpoint_deny(self): | ||||
|         """Test request (deny on endpoint level)""" | ||||
|         PolicyBinding.objects.create( | ||||
| @ -74,6 +118,7 @@ class TestRACViews(APITestCase): | ||||
|             policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2), | ||||
|             order=0, | ||||
|         ) | ||||
|         License.objects.create(key=generate_id()) | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
| @ -4,14 +4,14 @@ from channels.auth import AuthMiddleware | ||||
| from channels.sessions import CookieMiddleware | ||||
| from django.urls import path | ||||
| 
 | ||||
| from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet | ||||
| from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet | ||||
| from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet | ||||
| from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet | ||||
| from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer | ||||
| from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer | ||||
| from authentik.enterprise.providers.rac.views import RACInterface, RACStartView | ||||
| from authentik.outposts.channels import TokenOutpostMiddleware | ||||
| from authentik.providers.rac.api.connection_tokens import ConnectionTokenViewSet | ||||
| from authentik.providers.rac.api.endpoints import EndpointViewSet | ||||
| from authentik.providers.rac.api.property_mappings import RACPropertyMappingViewSet | ||||
| from authentik.providers.rac.api.providers import RACProviderViewSet | ||||
| from authentik.providers.rac.consumer_client import RACClientConsumer | ||||
| from authentik.providers.rac.consumer_outpost import RACOutpostConsumer | ||||
| from authentik.providers.rac.views import RACInterface, RACStartView | ||||
| from authentik.root.asgi_middleware import SessionMiddleware | ||||
| from authentik.root.middleware import ChannelsLoggingMiddleware | ||||
| 
 | ||||
| @ -10,6 +10,8 @@ from django.utils.translation import gettext as _ | ||||
| 
 | ||||
| from authentik.core.models import Application, AuthenticatedSession | ||||
| from authentik.core.views.interface import InterfaceView | ||||
| from authentik.enterprise.policy import EnterprisePolicyAccessView | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint, RACProvider | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.challenge import RedirectChallenge | ||||
| from authentik.flows.exceptions import FlowNonApplicableException | ||||
| @ -18,11 +20,9 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner | ||||
| from authentik.flows.stage import RedirectStage | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | ||||
| 
 | ||||
| 
 | ||||
| class RACStartView(PolicyAccessView): | ||||
| class RACStartView(EnterprisePolicyAccessView): | ||||
|     """Start a RAC connection by checking access and creating a connection token""" | ||||
| 
 | ||||
|     endpoint: Endpoint | ||||
| @ -16,6 +16,7 @@ TENANT_APPS = [ | ||||
|     "authentik.enterprise.audit", | ||||
|     "authentik.enterprise.providers.google_workspace", | ||||
|     "authentik.enterprise.providers.microsoft_entra", | ||||
|     "authentik.enterprise.providers.rac", | ||||
|     "authentik.enterprise.providers.ssf", | ||||
|     "authentik.enterprise.stages.authenticator_endpoint_gdtc", | ||||
|     "authentik.enterprise.stages.source", | ||||
|  | ||||
| @ -9,16 +9,13 @@ from django.utils.timezone import now | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.core.models import Source, User | ||||
| from authentik.core.sources.flow_manager import ( | ||||
|     SESSION_KEY_OVERRIDE_FLOW_TOKEN, | ||||
|     SESSION_KEY_SOURCE_FLOW_STAGES, | ||||
| ) | ||||
| from authentik.core.sources.flow_manager import SESSION_KEY_OVERRIDE_FLOW_TOKEN | ||||
| from authentik.core.types import UILoginButton | ||||
| from authentik.enterprise.stages.source.models import SourceStage | ||||
| from authentik.flows.challenge import Challenge, ChallengeResponse | ||||
| from authentik.flows.models import FlowToken, in_memory_stage | ||||
| from authentik.flows.models import FlowToken | ||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED | ||||
| from authentik.flows.stage import ChallengeStageView, StageView | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
|  | ||||
| PLAN_CONTEXT_RESUME_TOKEN = "resume_token"  # nosec | ||||
| @ -52,7 +49,6 @@ class SourceStageView(ChallengeStageView): | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         resume_token = self.create_flow_token() | ||||
|         self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token | ||||
|         self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)] | ||||
|         return self.login_button.challenge | ||||
|  | ||||
|     def create_flow_token(self) -> FlowToken: | ||||
| @ -81,19 +77,3 @@ class SourceStageView(ChallengeStageView): | ||||
|  | ||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
|  | ||||
| class SourceStageFinal(StageView): | ||||
|     """Dynamic stage injected in the source flow manager. This is injected in the | ||||
|     flow the source flow manager picks (authentication or enrollment), and will run at the end. | ||||
|     This stage uses the override flow token to resume execution of the initial flow the | ||||
|     source stage is bound to.""" | ||||
|  | ||||
|     def dispatch(self): | ||||
|         token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) | ||||
|         self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) | ||||
|         plan = token.plan | ||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = token | ||||
|         response = plan.to_redirect(self.request, token.flow) | ||||
|         token.delete() | ||||
|         return response | ||||
|  | ||||
| @ -1,54 +0,0 @@ | ||||
| """Email utility functions""" | ||||
|  | ||||
|  | ||||
| def mask_email(email: str | None) -> str | None: | ||||
|     """Mask email address for privacy | ||||
|  | ||||
|     Args: | ||||
|         email: Email address to mask | ||||
|     Returns: | ||||
|         Masked email address or None if input is None | ||||
|     Example: | ||||
|         mask_email("myname@company.org") | ||||
|         'm*****@c******.org' | ||||
|     """ | ||||
|     if not email: | ||||
|         return None | ||||
|  | ||||
|     # Basic email format validation | ||||
|     if email.count("@") != 1: | ||||
|         raise ValueError("Invalid email format: Must contain exactly one '@' symbol") | ||||
|  | ||||
|     local, domain = email.split("@") | ||||
|     if not local or not domain: | ||||
|         raise ValueError("Invalid email format: Local and domain parts cannot be empty") | ||||
|  | ||||
|     domain_parts = domain.split(".") | ||||
|     if len(domain_parts) < 2:  # noqa: PLR2004 | ||||
|         raise ValueError("Invalid email format: Domain must contain at least one dot") | ||||
|  | ||||
|     limit = 2 | ||||
|  | ||||
|     # Mask local part (keep first char) | ||||
|     if len(local) <= limit: | ||||
|         masked_local = "*" * len(local) | ||||
|     else: | ||||
|         masked_local = local[0] + "*" * (len(local) - 1) | ||||
|  | ||||
|     # Mask each domain part except the last one (TLD) | ||||
|     masked_domain_parts = [] | ||||
|     for _i, part in enumerate(domain_parts[:-1]):  # Process all parts except TLD | ||||
|         if not part:  # Check for empty parts (consecutive dots) | ||||
|             raise ValueError("Invalid email format: Domain parts cannot be empty") | ||||
|         if len(part) <= limit: | ||||
|             masked_part = "*" * len(part) | ||||
|         else: | ||||
|             masked_part = part[0] + "*" * (len(part) - 1) | ||||
|         masked_domain_parts.append(masked_part) | ||||
|  | ||||
|     # Add TLD unchanged | ||||
|     if not domain_parts[-1]:  # Check if TLD is empty | ||||
|         raise ValueError("Invalid email format: TLD cannot be empty") | ||||
|     masked_domain_parts.append(domain_parts[-1]) | ||||
|  | ||||
|     return f"{masked_local}@{'.'.join(masked_domain_parts)}" | ||||
| @ -19,6 +19,7 @@ from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer | ||||
| from authentik.core.models import Provider | ||||
| from authentik.enterprise.license import LicenseKey | ||||
| from authentik.enterprise.providers.rac.models import RACProvider | ||||
| from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator | ||||
| from authentik.outposts.api.service_connections import ServiceConnectionSerializer | ||||
| from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME | ||||
| @ -30,7 +31,6 @@ from authentik.outposts.models import ( | ||||
| ) | ||||
| from authentik.providers.ldap.models import LDAPProvider | ||||
| from authentik.providers.proxy.models import ProxyProvider | ||||
| from authentik.providers.rac.models import RACProvider | ||||
| from authentik.providers.radius.models import RadiusProvider | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -18,6 +18,8 @@ from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION | ||||
| from structlog.stdlib import get_logger | ||||
| from yaml import safe_load | ||||
|  | ||||
| from authentik.enterprise.providers.rac.controllers.docker import RACDockerController | ||||
| from authentik.enterprise.providers.rac.controllers.kubernetes import RACKubernetesController | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask, prefill_task | ||||
| from authentik.lib.config import CONFIG | ||||
| @ -39,8 +41,6 @@ from authentik.providers.ldap.controllers.docker import LDAPDockerController | ||||
| from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController | ||||
| from authentik.providers.proxy.controllers.docker import ProxyDockerController | ||||
| from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController | ||||
| from authentik.providers.rac.controllers.docker import RACDockerController | ||||
| from authentik.providers.rac.controllers.kubernetes import RACKubernetesController | ||||
| from authentik.providers.radius.controllers.docker import RadiusDockerController | ||||
| from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| @ -1,11 +1,26 @@ | ||||
| """Expression Policy API""" | ||||
|  | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import CharField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.events.logs import LogEventSerializer, capture_logs | ||||
| from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer | ||||
| from authentik.policies.api.policies import PolicySerializer | ||||
| from authentik.policies.expression.evaluator import PolicyEvaluator | ||||
| from authentik.policies.expression.models import ExpressionPolicy | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.policies.process import PolicyProcess | ||||
| from authentik.policies.types import PolicyRequest | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class ExpressionPolicySerializer(PolicySerializer): | ||||
| @ -30,3 +45,50 @@ class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet): | ||||
|     filterset_fields = "__all__" | ||||
|     ordering = ["name"] | ||||
|     search_fields = ["name"] | ||||
|  | ||||
|     class ExpressionPolicyTestSerializer(PolicyTestSerializer): | ||||
|         """Expression policy test serializer""" | ||||
|  | ||||
|         expression = CharField() | ||||
|  | ||||
|     @permission_required("authentik_policies.view_policy") | ||||
|     @extend_schema( | ||||
|         request=ExpressionPolicyTestSerializer(), | ||||
|         responses={ | ||||
|             200: PolicyTestResultSerializer(), | ||||
|             400: OpenApiResponse(description="Invalid parameters"), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"]) | ||||
|     def test(self, request: Request, pk: str) -> Response: | ||||
|         """Test policy""" | ||||
|         policy = self.get_object() | ||||
|         test_params = self.ExpressionPolicyTestSerializer(data=request.data) | ||||
|         if not test_params.is_valid(): | ||||
|             return Response(test_params.errors, status=400) | ||||
|  | ||||
|         # User permission check, only allow policy testing for users that are readable | ||||
|         users = get_objects_for_user(request.user, "authentik_core.view_user").filter( | ||||
|             pk=test_params.validated_data["user"].pk | ||||
|         ) | ||||
|         if not users.exists(): | ||||
|             return Response(status=400) | ||||
|  | ||||
|         policy.expression = test_params.validated_data["expression"] | ||||
|  | ||||
|         p_request = PolicyRequest(users.first()) | ||||
|         p_request.debug = True | ||||
|         p_request.set_http_request(self.request) | ||||
|         p_request.context = test_params.validated_data.get("context", {}) | ||||
|  | ||||
|         proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None) | ||||
|         with capture_logs() as logs: | ||||
|             result = proc.execute() | ||||
|         log_messages = [] | ||||
|         for log in logs: | ||||
|             if log.attributes.get("process", "") == "PolicyProcess": | ||||
|                 continue | ||||
|             log_messages.append(LogEventSerializer(log).data) | ||||
|         result.log_messages = log_messages | ||||
|         response = PolicyTestResultSerializer(result) | ||||
|         return Response(response.data) | ||||
|  | ||||
| @ -42,12 +42,6 @@ class GeoIPPolicySerializer(CountryFieldMixin, PolicySerializer): | ||||
|             "asns", | ||||
|             "countries", | ||||
|             "countries_obj", | ||||
|             "check_history_distance", | ||||
|             "history_max_distance_km", | ||||
|             "distance_tolerance_km", | ||||
|             "history_login_count", | ||||
|             "check_impossible_travel", | ||||
|             "impossible_tolerance_km", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,43 +0,0 @@ | ||||
| # Generated by Django 5.0.10 on 2025-01-02 20:40 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_policies_geoip", "0001_initial"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="geoippolicy", | ||||
|             name="check_history_distance", | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="geoippolicy", | ||||
|             name="check_impossible_travel", | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="geoippolicy", | ||||
|             name="distance_tolerance_km", | ||||
|             field=models.PositiveIntegerField(default=50), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="geoippolicy", | ||||
|             name="history_login_count", | ||||
|             field=models.PositiveIntegerField(default=5), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="geoippolicy", | ||||
|             name="history_max_distance_km", | ||||
|             field=models.PositiveBigIntegerField(default=100), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="geoippolicy", | ||||
|             name="impossible_tolerance_km", | ||||
|             field=models.PositiveIntegerField(default=100), | ||||
|         ), | ||||
|     ] | ||||
| @ -4,21 +4,15 @@ from itertools import chain | ||||
|  | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext as _ | ||||
| from django_countries.fields import CountryField | ||||
| from geopy import distance | ||||
| from rest_framework.serializers import BaseSerializer | ||||
|  | ||||
| from authentik.events.context_processors.geoip import GeoIPDict | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.policies.exceptions import PolicyException | ||||
| from authentik.policies.geoip.exceptions import GeoIPNotFoundException | ||||
| from authentik.policies.models import Policy | ||||
| from authentik.policies.types import PolicyRequest, PolicyResult | ||||
|  | ||||
| MAX_DISTANCE_HOUR_KM = 1000 | ||||
|  | ||||
|  | ||||
| class GeoIPPolicy(Policy): | ||||
|     """Ensure the user satisfies requirements of geography or network topology, based on IP | ||||
| @ -27,15 +21,6 @@ class GeoIPPolicy(Policy): | ||||
|     asns = ArrayField(models.IntegerField(), blank=True, default=list) | ||||
|     countries = CountryField(multiple=True, blank=True) | ||||
|  | ||||
|     distance_tolerance_km = models.PositiveIntegerField(default=50) | ||||
|  | ||||
|     check_history_distance = models.BooleanField(default=False) | ||||
|     history_max_distance_km = models.PositiveBigIntegerField(default=100) | ||||
|     history_login_count = models.PositiveIntegerField(default=5) | ||||
|  | ||||
|     check_impossible_travel = models.BooleanField(default=False) | ||||
|     impossible_tolerance_km = models.PositiveIntegerField(default=100) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[BaseSerializer]: | ||||
|         from authentik.policies.geoip.api import GeoIPPolicySerializer | ||||
| @ -52,27 +37,21 @@ class GeoIPPolicy(Policy): | ||||
|         - the client IP is advertised by an autonomous system with ASN in the `asns` | ||||
|         - the client IP is geolocated in a country of `countries` | ||||
|         """ | ||||
|         static_results: list[PolicyResult] = [] | ||||
|         dynamic_results: list[PolicyResult] = [] | ||||
|         results: list[PolicyResult] = [] | ||||
|  | ||||
|         if self.asns: | ||||
|             static_results.append(self.passes_asn(request)) | ||||
|             results.append(self.passes_asn(request)) | ||||
|         if self.countries: | ||||
|             static_results.append(self.passes_country(request)) | ||||
|             results.append(self.passes_country(request)) | ||||
|  | ||||
|         if self.check_history_distance or self.check_impossible_travel: | ||||
|             dynamic_results.append(self.passes_distance(request)) | ||||
|  | ||||
|         if not static_results and not dynamic_results: | ||||
|         if not results: | ||||
|             return PolicyResult(True) | ||||
|  | ||||
|         passing = any(r.passing for r in static_results) and all(r.passing for r in dynamic_results) | ||||
|         messages = chain( | ||||
|             *[r.messages for r in static_results], *[r.messages for r in dynamic_results] | ||||
|         ) | ||||
|         passing = any(r.passing for r in results) | ||||
|         messages = chain(*[r.messages for r in results]) | ||||
|  | ||||
|         result = PolicyResult(passing, *messages) | ||||
|         result.source_results = list(chain(static_results, dynamic_results)) | ||||
|         result.source_results = results | ||||
|  | ||||
|         return result | ||||
|  | ||||
| @ -94,7 +73,7 @@ class GeoIPPolicy(Policy): | ||||
|  | ||||
|     def passes_country(self, request: PolicyRequest) -> PolicyResult: | ||||
|         # This is not a single get chain because `request.context` can contain `{ "geoip": None }`. | ||||
|         geoip_data: GeoIPDict | None = request.context.get("geoip") | ||||
|         geoip_data = request.context.get("geoip") | ||||
|         country = geoip_data.get("country") if geoip_data else None | ||||
|  | ||||
|         if not country: | ||||
| @ -108,42 +87,6 @@ class GeoIPPolicy(Policy): | ||||
|  | ||||
|         return PolicyResult(True) | ||||
|  | ||||
|     def passes_distance(self, request: PolicyRequest) -> PolicyResult: | ||||
|         """Check if current policy execution is out of distance range compared | ||||
|         to previous authentication requests""" | ||||
|         # Get previous login event and GeoIP data | ||||
|         previous_logins = Event.objects.filter( | ||||
|             action=EventAction.LOGIN, user__pk=request.user.pk, context__geo__isnull=False | ||||
|         ).order_by("-created")[: self.history_login_count] | ||||
|         _now = now() | ||||
|         geoip_data: GeoIPDict | None = request.context.get("geoip") | ||||
|         if not geoip_data: | ||||
|             return PolicyResult(False) | ||||
|         for previous_login in previous_logins: | ||||
|             previous_login_geoip: GeoIPDict = previous_login.context["geo"] | ||||
|  | ||||
|             # Figure out distance | ||||
|             dist = distance.geodesic( | ||||
|                 (previous_login_geoip["lat"], previous_login_geoip["long"]), | ||||
|                 (geoip_data["lat"], geoip_data["long"]), | ||||
|             ) | ||||
|             if self.check_history_distance and dist.km >= ( | ||||
|                 self.history_max_distance_km + self.distance_tolerance_km | ||||
|             ): | ||||
|                 return PolicyResult( | ||||
|                     False, _("Distance from previous authentication is larger than threshold.") | ||||
|                 ) | ||||
|             # Check if distance between `previous_login` and now is more | ||||
|             # than max distance per hour times the amount of hours since the previous login | ||||
|             # (round down to the lowest closest time of hours) | ||||
|             # clamped to be at least 1 hour | ||||
|             rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1) | ||||
|             if self.check_impossible_travel and dist.km >= ( | ||||
|                 (MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km | ||||
|             ): | ||||
|                 return PolicyResult(False, _("Distance is further than possible.")) | ||||
|         return PolicyResult(True) | ||||
|  | ||||
|     class Meta(Policy.PolicyMeta): | ||||
|         verbose_name = _("GeoIP Policy") | ||||
|         verbose_name_plural = _("GeoIP Policies") | ||||
|  | ||||
| @ -1,10 +1,8 @@ | ||||
| """geoip policy tests""" | ||||
|  | ||||
| from django.test import TestCase | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_user | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.utils import get_user | ||||
| from authentik.policies.engine import PolicyRequest, PolicyResult | ||||
| from authentik.policies.exceptions import PolicyException | ||||
| from authentik.policies.geoip.exceptions import GeoIPNotFoundException | ||||
| @ -16,8 +14,8 @@ class TestGeoIPPolicy(TestCase): | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.user = create_test_user() | ||||
|         self.request = PolicyRequest(self.user) | ||||
|  | ||||
|         self.request = PolicyRequest(get_anonymous_user()) | ||||
|  | ||||
|         self.context_disabled_geoip = {} | ||||
|         self.context_unknown_ip = {"asn": None, "geoip": None} | ||||
| @ -128,70 +126,3 @@ class TestGeoIPPolicy(TestCase): | ||||
|         result: PolicyResult = policy.passes(self.request) | ||||
|  | ||||
|         self.assertTrue(result.passing) | ||||
|  | ||||
|     def test_history(self): | ||||
|         """Test history checks""" | ||||
|         Event.objects.create( | ||||
|             action=EventAction.LOGIN, | ||||
|             user=get_user(self.user), | ||||
|             context={ | ||||
|                 # Random location in Canada | ||||
|                 "geo": {"lat": 55.868351, "long": -104.441011}, | ||||
|             }, | ||||
|         ) | ||||
|         # Random location in Poland | ||||
|         self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679} | ||||
|  | ||||
|         policy = GeoIPPolicy.objects.create(check_history_distance=True) | ||||
|  | ||||
|         result: PolicyResult = policy.passes(self.request) | ||||
|         self.assertFalse(result.passing) | ||||
|  | ||||
|     def test_history_no_data(self): | ||||
|         """Test history checks (with no geoip data in context)""" | ||||
|         Event.objects.create( | ||||
|             action=EventAction.LOGIN, | ||||
|             user=get_user(self.user), | ||||
|             context={ | ||||
|                 # Random location in Canada | ||||
|                 "geo": {"lat": 55.868351, "long": -104.441011}, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         policy = GeoIPPolicy.objects.create(check_history_distance=True) | ||||
|  | ||||
|         result: PolicyResult = policy.passes(self.request) | ||||
|         self.assertFalse(result.passing) | ||||
|  | ||||
|     def test_history_impossible_travel(self): | ||||
|         """Test history checks""" | ||||
|         Event.objects.create( | ||||
|             action=EventAction.LOGIN, | ||||
|             user=get_user(self.user), | ||||
|             context={ | ||||
|                 # Random location in Canada | ||||
|                 "geo": {"lat": 55.868351, "long": -104.441011}, | ||||
|             }, | ||||
|         ) | ||||
|         # Random location in Poland | ||||
|         self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679} | ||||
|  | ||||
|         policy = GeoIPPolicy.objects.create(check_impossible_travel=True) | ||||
|  | ||||
|         result: PolicyResult = policy.passes(self.request) | ||||
|         self.assertFalse(result.passing) | ||||
|  | ||||
|     def test_history_no_geoip(self): | ||||
|         """Test history checks (previous login with no geoip data)""" | ||||
|         Event.objects.create( | ||||
|             action=EventAction.LOGIN, | ||||
|             user=get_user(self.user), | ||||
|             context={}, | ||||
|         ) | ||||
|         # Random location in Poland | ||||
|         self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679} | ||||
|  | ||||
|         policy = GeoIPPolicy.objects.create(check_history_distance=True) | ||||
|  | ||||
|         result: PolicyResult = policy.passes(self.request) | ||||
|         self.assertFalse(result.passing) | ||||
|  | ||||
| @ -148,10 +148,10 @@ class PasswordPolicy(Policy): | ||||
|             user_inputs.append(request.user.email) | ||||
|         if request.http_request: | ||||
|             user_inputs.append(request.http_request.brand.branding_title) | ||||
|         # Only calculate result for the first 72 characters, as with over 100 char | ||||
|         # Only calculate result for the first 100 characters, as with over 100 char | ||||
|         # long passwords we can be reasonably sure that they'll surpass the score anyways | ||||
|         # See https://github.com/dropbox/zxcvbn#runtime-latency | ||||
|         results = zxcvbn(password[:72], user_inputs) | ||||
|         results = zxcvbn(password[:100], user_inputs) | ||||
|         LOGGER.debug("password failed", check="zxcvbn", score=results["score"]) | ||||
|         result = PolicyResult(results["score"] > self.zxcvbn_score_threshold) | ||||
|         if not result.passing: | ||||
|  | ||||
| @ -1,14 +0,0 @@ | ||||
| """RAC app config""" | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikProviderRAC(AppConfig): | ||||
|     """authentik rac app config""" | ||||
|  | ||||
|     name = "authentik.providers.rac" | ||||
|     label = "authentik_providers_rac" | ||||
|     verbose_name = "authentik Providers.RAC" | ||||
|     default = True | ||||
|     mountpoint = "" | ||||
|     ws_mountpoint = "authentik.providers.rac.urls" | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from django.apps import apps | ||||
| from django.contrib.auth.models import Permission | ||||
| from django.db.models import QuerySet | ||||
| from django.db.models import Q, QuerySet | ||||
| from django_filters.filters import ModelChoiceFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| @ -18,6 +18,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
|  | ||||
| from authentik.blueprints.v1.importer import excluded_models | ||||
| from authentik.core.api.utils import ModelSerializer, PassiveSerializer | ||||
| from authentik.core.models import User | ||||
| from authentik.lib.validators import RequiredTogetherValidator | ||||
| @ -105,13 +106,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet): | ||||
|     ] | ||||
|  | ||||
|     def get_queryset(self) -> QuerySet: | ||||
|         return ( | ||||
|             Permission.objects.all() | ||||
|             .select_related("content_type") | ||||
|             .filter( | ||||
|                 content_type__app_label__startswith="authentik", | ||||
|         query = Q() | ||||
|         for model in excluded_models(): | ||||
|             query |= Q( | ||||
|                 content_type__app_label=model._meta.app_label, | ||||
|                 content_type__model=model._meta.model_name, | ||||
|             ) | ||||
|         ) | ||||
|         return Permission.objects.all().select_related("content_type").exclude(query) | ||||
|  | ||||
|  | ||||
| class PermissionAssignSerializer(PassiveSerializer): | ||||
|  | ||||
| @ -87,7 +87,6 @@ TENANT_APPS = [ | ||||
|     "authentik.providers.ldap", | ||||
|     "authentik.providers.oauth2", | ||||
|     "authentik.providers.proxy", | ||||
|     "authentik.providers.rac", | ||||
|     "authentik.providers.radius", | ||||
|     "authentik.providers.saml", | ||||
|     "authentik.providers.scim", | ||||
| @ -101,7 +100,6 @@ TENANT_APPS = [ | ||||
|     "authentik.sources.scim", | ||||
|     "authentik.stages.authenticator", | ||||
|     "authentik.stages.authenticator_duo", | ||||
|     "authentik.stages.authenticator_email", | ||||
|     "authentik.stages.authenticator_sms", | ||||
|     "authentik.stages.authenticator_static", | ||||
|     "authentik.stages.authenticator_totp", | ||||
|  | ||||
| @ -2,7 +2,6 @@ | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| from requests import RequestException | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient | ||||
| @ -22,35 +21,10 @@ class AzureADOAuthRedirect(OAuthRedirect): | ||||
|         } | ||||
|  | ||||
|  | ||||
| class AzureADClient(UserprofileHeaderAuthClient): | ||||
|     """Fetch AzureAD group information""" | ||||
|  | ||||
|     def get_profile_info(self, token): | ||||
|         profile_data = super().get_profile_info(token) | ||||
|         if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes: | ||||
|             return profile_data | ||||
|         group_response = self.session.request( | ||||
|             "get", | ||||
|             "https://graph.microsoft.com/v1.0/me/memberOf", | ||||
|             headers={"Authorization": f"{token['token_type']} {token['access_token']}"}, | ||||
|         ) | ||||
|         try: | ||||
|             group_response.raise_for_status() | ||||
|         except RequestException as exc: | ||||
|             LOGGER.warning( | ||||
|                 "Unable to fetch user profile", | ||||
|                 exc=exc, | ||||
|                 response=exc.response.text if exc.response else str(exc), | ||||
|             ) | ||||
|             return None | ||||
|         profile_data["raw_groups"] = group_response.json() | ||||
|         return profile_data | ||||
|  | ||||
|  | ||||
| class AzureADOAuthCallback(OpenIDConnectOAuth2Callback): | ||||
|     """AzureAD OAuth2 Callback""" | ||||
|  | ||||
|     client_class = AzureADClient | ||||
|     client_class = UserprofileHeaderAuthClient | ||||
|  | ||||
|     def get_user_id(self, info: dict[str, str]) -> str: | ||||
|         # Default try to get `id` for the Graph API endpoint | ||||
| @ -79,24 +53,8 @@ class AzureADType(SourceType): | ||||
|  | ||||
|     def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]: | ||||
|         mail = info.get("mail", None) or info.get("otherMails", [None])[0] | ||||
|         # Format group info | ||||
|         groups = [] | ||||
|         group_id_dict = {} | ||||
|         for group in info.get("raw_groups", {}).get("value", []): | ||||
|             if group["@odata.type"] != "#microsoft.graph.group": | ||||
|                 continue | ||||
|             groups.append(group["id"]) | ||||
|             group_id_dict[group["id"]] = group | ||||
|         info["raw_groups"] = group_id_dict | ||||
|         return { | ||||
|             "username": info.get("userPrincipalName"), | ||||
|             "email": mail, | ||||
|             "name": info.get("displayName"), | ||||
|             "groups": groups, | ||||
|         } | ||||
|  | ||||
|     def get_base_group_properties(self, source, group_id, **kwargs): | ||||
|         raw_group = kwargs["info"]["raw_groups"][group_id] | ||||
|         return { | ||||
|             "name": raw_group["displayName"], | ||||
|         } | ||||
|  | ||||
| @ -1,85 +0,0 @@ | ||||
| """AuthenticatorEmailStage API Views""" | ||||
|  | ||||
| from rest_framework import mixins | ||||
| from rest_framework.viewsets import GenericViewSet, ModelViewSet | ||||
|  | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import ModelSerializer | ||||
| from authentik.flows.api.stages import StageSerializer | ||||
| from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice | ||||
|  | ||||
|  | ||||
| class AuthenticatorEmailStageSerializer(StageSerializer): | ||||
|     """AuthenticatorEmailStage Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|         model = AuthenticatorEmailStage | ||||
|         fields = StageSerializer.Meta.fields + [ | ||||
|             "configure_flow", | ||||
|             "friendly_name", | ||||
|             "use_global_settings", | ||||
|             "host", | ||||
|             "port", | ||||
|             "username", | ||||
|             "password", | ||||
|             "use_tls", | ||||
|             "use_ssl", | ||||
|             "timeout", | ||||
|             "from_address", | ||||
|             "subject", | ||||
|             "token_expiry", | ||||
|             "template", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class AuthenticatorEmailStageViewSet(UsedByMixin, ModelViewSet): | ||||
|     """AuthenticatorEmailStage Viewset""" | ||||
|  | ||||
|     queryset = AuthenticatorEmailStage.objects.all() | ||||
|     serializer_class = AuthenticatorEmailStageSerializer | ||||
|     filterset_fields = "__all__" | ||||
|     ordering = ["name"] | ||||
|     search_fields = ["name"] | ||||
|  | ||||
|  | ||||
| class EmailDeviceSerializer(ModelSerializer): | ||||
|     """Serializer for email authenticator devices""" | ||||
|  | ||||
|     user = GroupMemberSerializer(read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = EmailDevice | ||||
|         fields = ["name", "pk", "email", "user"] | ||||
|         depth = 2 | ||||
|         extra_kwargs = { | ||||
|             "email": {"read_only": True}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class EmailDeviceViewSet( | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.UpdateModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     UsedByMixin, | ||||
|     mixins.ListModelMixin, | ||||
|     GenericViewSet, | ||||
| ): | ||||
|     """Viewset for email authenticator devices""" | ||||
|  | ||||
|     queryset = EmailDevice.objects.all() | ||||
|     serializer_class = EmailDeviceSerializer | ||||
|     search_fields = ["name"] | ||||
|     filterset_fields = ["name"] | ||||
|     ordering = ["name"] | ||||
|     owner_field = "user" | ||||
|  | ||||
|  | ||||
| class EmailAdminDeviceViewSet(ModelViewSet): | ||||
|     """Viewset for email authenticator devices (for admins)""" | ||||
|  | ||||
|     queryset = EmailDevice.objects.all() | ||||
|     serializer_class = EmailDeviceSerializer | ||||
|     search_fields = ["name"] | ||||
|     filterset_fields = ["name"] | ||||
|     ordering = ["name"] | ||||
| @ -1,12 +0,0 @@ | ||||
| """Email Authenticator""" | ||||
|  | ||||
| from authentik.blueprints.apps import ManagedAppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikStageAuthenticatorEmailConfig(ManagedAppConfig): | ||||
|     """Email Authenticator App config""" | ||||
|  | ||||
|     name = "authentik.stages.authenticator_email" | ||||
|     label = "authentik_stages_authenticator_email" | ||||
|     verbose_name = "authentik Stages.Authenticator.Email" | ||||
|     default = True | ||||
| @ -1,132 +0,0 @@ | ||||
| # Generated by Django 5.0.10 on 2025-01-27 20:05 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| import django.utils.timezone | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import authentik.lib.utils.time | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     initial = True | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_flows", "0027_auto_20231028_1424"), | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="AuthenticatorEmailStage", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "stage_ptr", | ||||
|                     models.OneToOneField( | ||||
|                         auto_created=True, | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         parent_link=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         to="authentik_flows.stage", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("friendly_name", models.TextField(null=True)), | ||||
|                 ( | ||||
|                     "use_global_settings", | ||||
|                     models.BooleanField( | ||||
|                         default=False, | ||||
|                         help_text="When enabled, global Email connection settings will be used and connection settings below will be ignored.", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("host", models.TextField(default="localhost")), | ||||
|                 ("port", models.IntegerField(default=25)), | ||||
|                 ("username", models.TextField(blank=True, default="")), | ||||
|                 ("password", models.TextField(blank=True, default="")), | ||||
|                 ("use_tls", models.BooleanField(default=False)), | ||||
|                 ("use_ssl", models.BooleanField(default=False)), | ||||
|                 ("timeout", models.IntegerField(default=10)), | ||||
|                 ( | ||||
|                     "from_address", | ||||
|                     models.EmailField(default="system@authentik.local", max_length=254), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "token_expiry", | ||||
|                     models.TextField( | ||||
|                         default="minutes=30", | ||||
|                         help_text="Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).", | ||||
|                         validators=[authentik.lib.utils.time.timedelta_string_validator], | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("subject", models.TextField(default="authentik Sign-in code")), | ||||
|                 ("template", models.TextField(default="email/email_otp.html")), | ||||
|                 ( | ||||
|                     "configure_flow", | ||||
|                     models.ForeignKey( | ||||
|                         blank=True, | ||||
|                         help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.", | ||||
|                         null=True, | ||||
|                         on_delete=django.db.models.deletion.SET_NULL, | ||||
|                         to="authentik_flows.flow", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Email Authenticator Setup Stage", | ||||
|                 "verbose_name_plural": "Email Authenticator Setup Stages", | ||||
|             }, | ||||
|             bases=("authentik_flows.stage", models.Model), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="EmailDevice", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.AutoField( | ||||
|                         auto_created=True, primary_key=True, serialize=False, verbose_name="ID" | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("created", models.DateTimeField(auto_now_add=True)), | ||||
|                 ("last_updated", models.DateTimeField(auto_now=True)), | ||||
|                 ( | ||||
|                     "name", | ||||
|                     models.CharField( | ||||
|                         help_text="The human-readable name of this device.", max_length=64 | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "confirmed", | ||||
|                     models.BooleanField(default=True, help_text="Is this device ready for use?"), | ||||
|                 ), | ||||
|                 ("token", models.CharField(blank=True, max_length=16, null=True)), | ||||
|                 ( | ||||
|                     "valid_until", | ||||
|                     models.DateTimeField( | ||||
|                         default=django.utils.timezone.now, | ||||
|                         help_text="The timestamp of the moment of expiry of the saved token.", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("email", models.EmailField(max_length=254)), | ||||
|                 ("last_used", models.DateTimeField(auto_now=True)), | ||||
|                 ( | ||||
|                     "stage", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         to="authentik_stages_authenticator_email.authenticatoremailstage", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "user", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Email Device", | ||||
|                 "verbose_name_plural": "Email Devices", | ||||
|                 "unique_together": {("user", "email")}, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -1,167 +0,0 @@ | ||||
| from django.contrib.auth import get_user_model | ||||
| from django.core.mail.backends.base import BaseEmailBackend | ||||
| from django.core.mail.backends.smtp import EmailBackend | ||||
| from django.db import models | ||||
| from django.template import TemplateSyntaxError | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views import View | ||||
| from rest_framework.serializers import BaseSerializer | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.exceptions import StageInvalidException | ||||
| from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.models import SerializerModel | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.time import timedelta_string_validator | ||||
| from authentik.stages.authenticator.models import SideChannelDevice | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
|  | ||||
|  | ||||
| class EmailTemplates(models.TextChoices): | ||||
|     """Templates used for rendering the Email""" | ||||
|  | ||||
|     EMAIL_OTP = ( | ||||
|         "email/email_otp.html", | ||||
|         _("Email OTP"), | ||||
|     )  # nosec | ||||
|  | ||||
|  | ||||
| class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage): | ||||
|     """Use Email-based authentication instead of authenticator-based.""" | ||||
|  | ||||
|     use_global_settings = models.BooleanField( | ||||
|         default=False, | ||||
|         help_text=_( | ||||
|             "When enabled, global Email connection settings will be used and " | ||||
|             "connection settings below will be ignored." | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     host = models.TextField(default="localhost") | ||||
|     port = models.IntegerField(default=25) | ||||
|     username = models.TextField(default="", blank=True) | ||||
|     password = models.TextField(default="", blank=True) | ||||
|     use_tls = models.BooleanField(default=False) | ||||
|     use_ssl = models.BooleanField(default=False) | ||||
|     timeout = models.IntegerField(default=10) | ||||
|     from_address = models.EmailField(default="system@authentik.local") | ||||
|  | ||||
|     token_expiry = models.TextField( | ||||
|         default="minutes=30", | ||||
|         validators=[timedelta_string_validator], | ||||
|         help_text=_("Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."), | ||||
|     ) | ||||
|     subject = models.TextField(default="authentik Sign-in code") | ||||
|     template = models.TextField(default=EmailTemplates.EMAIL_OTP) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[BaseSerializer]: | ||||
|         from authentik.stages.authenticator_email.api import AuthenticatorEmailStageSerializer | ||||
|  | ||||
|         return AuthenticatorEmailStageSerializer | ||||
|  | ||||
|     @property | ||||
|     def view(self) -> type[View]: | ||||
|         from authentik.stages.authenticator_email.stage import AuthenticatorEmailStageView | ||||
|  | ||||
|         return AuthenticatorEmailStageView | ||||
|  | ||||
|     @property | ||||
|     def component(self) -> str: | ||||
|         return "ak-stage-authenticator-email-form" | ||||
|  | ||||
|     @property | ||||
|     def backend_class(self) -> type[BaseEmailBackend]: | ||||
|         """Get the email backend class to use""" | ||||
|         return EmailBackend | ||||
|  | ||||
|     @property | ||||
|     def backend(self) -> BaseEmailBackend: | ||||
|         """Get fully configured Email Backend instance""" | ||||
|         if self.use_global_settings: | ||||
|             CONFIG.refresh("email.password") | ||||
|             return self.backend_class( | ||||
|                 host=CONFIG.get("email.host"), | ||||
|                 port=CONFIG.get_int("email.port"), | ||||
|                 username=CONFIG.get("email.username"), | ||||
|                 password=CONFIG.get("email.password"), | ||||
|                 use_tls=CONFIG.get_bool("email.use_tls", False), | ||||
|                 use_ssl=CONFIG.get_bool("email.use_ssl", False), | ||||
|                 timeout=CONFIG.get_int("email.timeout"), | ||||
|             ) | ||||
|         return self.backend_class( | ||||
|             host=self.host, | ||||
|             port=self.port, | ||||
|             username=self.username, | ||||
|             password=self.password, | ||||
|             use_tls=self.use_tls, | ||||
|             use_ssl=self.use_ssl, | ||||
|             timeout=self.timeout, | ||||
|         ) | ||||
|  | ||||
|     def send(self, device: "EmailDevice"): | ||||
|         # Lazy import here to avoid circular import | ||||
|         from authentik.stages.email.tasks import send_mails | ||||
|  | ||||
|         # Compose the message using templates | ||||
|         message = device._compose_email() | ||||
|         return send_mails(device.stage, message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"Email Authenticator Stage {self.name}" | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("Email Authenticator Setup Stage") | ||||
|         verbose_name_plural = _("Email Authenticator Setup Stages") | ||||
|  | ||||
|  | ||||
| class EmailDevice(SerializerModel, SideChannelDevice): | ||||
|     """Email Device""" | ||||
|  | ||||
|     user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) | ||||
|     email = models.EmailField() | ||||
|     stage = models.ForeignKey(AuthenticatorEmailStage, on_delete=models.CASCADE) | ||||
|     last_used = models.DateTimeField(auto_now=True) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[BaseSerializer]: | ||||
|         from authentik.stages.authenticator_email.api import EmailDeviceSerializer | ||||
|  | ||||
|         return EmailDeviceSerializer | ||||
|  | ||||
|     def _compose_email(self) -> TemplateEmailMessage: | ||||
|         try: | ||||
|             pending_user = self.user | ||||
|             stage = self.stage | ||||
|             email = self.email | ||||
|  | ||||
|             message = TemplateEmailMessage( | ||||
|                 subject=_(stage.subject), | ||||
|                 to=[(pending_user.name, email)], | ||||
|                 template_name=stage.template, | ||||
|                 template_context={ | ||||
|                     "user": pending_user, | ||||
|                     "expires": self.valid_until, | ||||
|                     "token": self.token, | ||||
|                 }, | ||||
|             ) | ||||
|             return message | ||||
|         except TemplateSyntaxError as exc: | ||||
|             Event.new( | ||||
|                 EventAction.CONFIGURATION_ERROR, | ||||
|                 message=_("Exception occurred while rendering E-mail template"), | ||||
|                 error=exception_to_string(exc), | ||||
|                 template=stage.template, | ||||
|             ).from_http(self.request) | ||||
|             raise StageInvalidException from exc | ||||
|  | ||||
|     def __str__(self): | ||||
|         if not self.pk: | ||||
|             return "New Email Device" | ||||
|         return f"Email Device for {self.user_id}" | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("Email Device") | ||||
|         verbose_name_plural = _("Email Devices") | ||||
|         unique_together = (("user", "email"),) | ||||
| @ -1,177 +0,0 @@ | ||||
| """Email Setup stage""" | ||||
|  | ||||
| from django.db.models import Q | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.http.request import QueryDict | ||||
| from django.template.exceptions import TemplateSyntaxError | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.fields import BooleanField, CharField, IntegerField | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.challenge import ( | ||||
|     Challenge, | ||||
|     ChallengeResponse, | ||||
|     WithUserInfoChallenge, | ||||
| ) | ||||
| from authentik.flows.exceptions import StageInvalidException | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.lib.utils.email import mask_email | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.stages.authenticator_email.models import ( | ||||
|     AuthenticatorEmailStage, | ||||
|     EmailDevice, | ||||
| ) | ||||
| from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||
|  | ||||
| SESSION_KEY_EMAIL_DEVICE = "authentik/stages/authenticator_email/email_device" | ||||
| PLAN_CONTEXT_EMAIL = "email" | ||||
| PLAN_CONTEXT_EMAIL_SENT = "email_sent" | ||||
| PLAN_CONTEXT_EMAIL_OVERRIDE = "email" | ||||
|  | ||||
|  | ||||
| class AuthenticatorEmailChallenge(WithUserInfoChallenge): | ||||
|     """Authenticator Email Setup challenge""" | ||||
|  | ||||
|     # Set to true if no previous prompt stage set the email | ||||
|     # this stage will also check prompt_data.email | ||||
|     email = CharField(default=None, allow_blank=True, allow_null=True) | ||||
|     email_required = BooleanField(default=True) | ||||
|     component = CharField(default="ak-stage-authenticator-email") | ||||
|  | ||||
|  | ||||
| class AuthenticatorEmailChallengeResponse(ChallengeResponse): | ||||
|     """Authenticator Email Challenge response, device is set by get_response_instance""" | ||||
|  | ||||
|     device: EmailDevice | ||||
|  | ||||
|     code = IntegerField(required=False) | ||||
|     email = CharField(required=False) | ||||
|  | ||||
|     component = CharField(default="ak-stage-authenticator-email") | ||||
|  | ||||
|     def validate(self, attrs: dict) -> dict: | ||||
|         """Check""" | ||||
|         if "code" not in attrs: | ||||
|             if "email" not in attrs: | ||||
|                 raise ValidationError("email required") | ||||
|             self.device.email = attrs["email"] | ||||
|             self.stage.validate_and_send(attrs["email"]) | ||||
|             return super().validate(attrs) | ||||
|         if not self.device.verify_token(str(attrs["code"])): | ||||
|             raise ValidationError(_("Code does not match")) | ||||
|         self.device.confirmed = True | ||||
|         return super().validate(attrs) | ||||
|  | ||||
|  | ||||
| class AuthenticatorEmailStageView(ChallengeStageView): | ||||
|     """Authenticator Email Setup stage""" | ||||
|  | ||||
|     response_class = AuthenticatorEmailChallengeResponse | ||||
|  | ||||
|     def validate_and_send(self, email: str): | ||||
|         """Validate email and send message""" | ||||
|         pending_user = self.get_pending_user() | ||||
|  | ||||
|         stage: AuthenticatorEmailStage = self.executor.current_stage | ||||
|         if EmailDevice.objects.filter(Q(email=email), stage=stage.pk).exists(): | ||||
|             raise ValidationError(_("Invalid email")) | ||||
|  | ||||
|         device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE] | ||||
|  | ||||
|         try: | ||||
|             message = TemplateEmailMessage( | ||||
|                 subject=_(stage.subject), | ||||
|                 to=[(pending_user.name, email)], | ||||
|                 language=pending_user.locale(self.request), | ||||
|                 template_name=stage.template, | ||||
|                 template_context={ | ||||
|                     "user": pending_user, | ||||
|                     "expires": device.valid_until, | ||||
|                     "token": device.token, | ||||
|                 }, | ||||
|             ) | ||||
|  | ||||
|             send_mails(stage, message) | ||||
|         except TemplateSyntaxError as exc: | ||||
|             Event.new( | ||||
|                 EventAction.CONFIGURATION_ERROR, | ||||
|                 message=_("Exception occurred while rendering E-mail template"), | ||||
|                 error=exception_to_string(exc), | ||||
|                 template=stage.template, | ||||
|             ).from_http(self.request) | ||||
|             raise StageInvalidException from exc | ||||
|  | ||||
|     def _has_email(self) -> str | None: | ||||
|         context = self.executor.plan.context | ||||
|  | ||||
|         # Check user's email attribute | ||||
|         user = self.get_pending_user() | ||||
|         if user.email: | ||||
|             self.logger.debug("got email from user attributes") | ||||
|             return user.email | ||||
|         # Check plan context for email | ||||
|         if PLAN_CONTEXT_EMAIL in context.get(PLAN_CONTEXT_PROMPT, {}): | ||||
|             self.logger.debug("got email from plan context") | ||||
|             return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_EMAIL) | ||||
|         # Check device for email | ||||
|         if SESSION_KEY_EMAIL_DEVICE in self.request.session: | ||||
|             self.logger.debug("got email from device in session") | ||||
|             device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE] | ||||
|             if device.email == "": | ||||
|                 return None | ||||
|             return device.email | ||||
|         return None | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> Challenge: | ||||
|         email = self._has_email() | ||||
|         return AuthenticatorEmailChallenge( | ||||
|             data={ | ||||
|                 "email": mask_email(email), | ||||
|                 "email_required": email is None, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     def get_response_instance(self, data: QueryDict) -> ChallengeResponse: | ||||
|         response = super().get_response_instance(data) | ||||
|         response.device = self.request.session[SESSION_KEY_EMAIL_DEVICE] | ||||
|         return response | ||||
|  | ||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         user = self.get_pending_user() | ||||
|  | ||||
|         stage: AuthenticatorEmailStage = self.executor.current_stage | ||||
|         if SESSION_KEY_EMAIL_DEVICE not in self.request.session: | ||||
|             device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device") | ||||
|             valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds() | ||||
|             device.generate_token(valid_secs=valid_secs, commit=False) | ||||
|             self.request.session[SESSION_KEY_EMAIL_DEVICE] = device | ||||
|             if email := self._has_email(): | ||||
|                 device.email = email | ||||
|                 try: | ||||
|                     self.validate_and_send(email) | ||||
|                 except ValidationError as exc: | ||||
|                     # We had an email given already (at this point only possible from flow | ||||
|                     # context), but an error occurred while sending (most likely) | ||||
|                     # due to a duplicate device, so delete the email we got given, reset the state | ||||
|                     # (ish) and retry | ||||
|                     device.email = "" | ||||
|                     self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop( | ||||
|                         PLAN_CONTEXT_EMAIL, None | ||||
|                     ) | ||||
|                     self.request.session.pop(SESSION_KEY_EMAIL_DEVICE, None) | ||||
|                     self.logger.warning("failed to send email to pre-set address", exc=exc) | ||||
|                     return self.get(request, *args, **kwargs) | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||
|         """Email Token is validated by challenge""" | ||||
|         device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE] | ||||
|         if not device.confirmed: | ||||
|             return self.challenge_invalid(response) | ||||
|         device.save() | ||||
|         del self.request.session[SESSION_KEY_EMAIL_DEVICE] | ||||
|         return self.executor.stage_ok() | ||||
| @ -1,44 +0,0 @@ | ||||
| {% extends "email/base.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load humanize %} | ||||
|  | ||||
| {% block content %} | ||||
| <tr> | ||||
|   <td align="center"> | ||||
|     <h1> | ||||
|       {% blocktrans with username=user.username %} | ||||
|       Hi {{ username }}, | ||||
|       {% endblocktrans %} | ||||
|     </h1> | ||||
|   </td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td align="center"> | ||||
|     <table border="0"> | ||||
|       <tr> | ||||
|         <td align="center" style="max-width: 300px; padding: 20px 0; color: #212124;"> | ||||
|           {% blocktrans %} | ||||
|           Email MFA code. | ||||
|           {% endblocktrans %} | ||||
|         </td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td align="center" class="btn btn-primary"> | ||||
|           {{ token }} | ||||
|         </td> | ||||
|       </tr> | ||||
|     </table> | ||||
|   </td> | ||||
| </tr> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block sub_content %} | ||||
| <tr> | ||||
|   <td style="padding: 20px; font-size: 12px; color: #212124;" align="center"> | ||||
|     {% blocktrans with expires=expires|timeuntil %} | ||||
|     If you did not request this code, please ignore this email. The code above is valid for {{ expires }}. | ||||
|     {% endblocktrans %} | ||||
|   </td> | ||||
| </tr> | ||||
| {% endblock %} | ||||
| @ -1,13 +0,0 @@ | ||||
| {% load i18n %}{% load humanize %}{% autoescape off %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %} | ||||
|  | ||||
| {% blocktrans %} | ||||
| Email MFA code | ||||
| {% endblocktrans %} | ||||
| {{ token }} | ||||
| {% blocktrans with expires=expires|timeuntil %} | ||||
| If you did not request this code, please ignore this email. The code above is valid for {{ expires }}. | ||||
| {% endblocktrans %} | ||||
|  | ||||
| --  | ||||
| Powered by goauthentik.io. | ||||
| {% endautoescape %} | ||||
| @ -1,340 +0,0 @@ | ||||
| """Test Email Authenticator API""" | ||||
|  | ||||
| from datetime import timedelta | ||||
| from unittest.mock import MagicMock, PropertyMock, patch | ||||
|  | ||||
| from django.core import mail | ||||
| from django.core.mail.backends.smtp import EmailBackend | ||||
| from django.db.utils import IntegrityError | ||||
| from django.template.exceptions import TemplateDoesNotExist | ||||
| from django.urls import reverse | ||||
| from django.utils.timezone import now | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user | ||||
| from authentik.flows.models import FlowStageBinding | ||||
| from authentik.flows.tests import FlowTestCase | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.email import mask_email | ||||
| from authentik.stages.authenticator_email.api import ( | ||||
|     AuthenticatorEmailStageSerializer, | ||||
|     EmailDeviceSerializer, | ||||
| ) | ||||
| from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice | ||||
| from authentik.stages.authenticator_email.stage import ( | ||||
|     SESSION_KEY_EMAIL_DEVICE, | ||||
| ) | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
|  | ||||
|  | ||||
| class TestAuthenticatorEmailStage(FlowTestCase): | ||||
|     """Test Email Authenticator stage""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.flow = create_test_flow() | ||||
|         self.user = create_test_admin_user() | ||||
|         self.user_noemail = create_test_user(email="") | ||||
|         self.stage = AuthenticatorEmailStage.objects.create( | ||||
|             name="email-authenticator", | ||||
|             use_global_settings=True, | ||||
|             from_address="test@authentik.local", | ||||
|             configure_flow=self.flow, | ||||
|             token_expiry="minutes=30", | ||||
|         )  # nosec | ||||
|         self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0) | ||||
|         self.device = EmailDevice.objects.create( | ||||
|             user=self.user, | ||||
|             stage=self.stage, | ||||
|             email="test@authentik.local", | ||||
|         ) | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_device_str(self): | ||||
|         """Test string representation of device""" | ||||
|         self.assertEqual(str(self.device), f"Email Device for {self.user.pk}") | ||||
|         # Test unsaved device | ||||
|         unsaved_device = EmailDevice( | ||||
|             user=self.user, | ||||
|             stage=self.stage, | ||||
|             email="test@authentik.local", | ||||
|         ) | ||||
|         self.assertEqual(str(unsaved_device), "New Email Device") | ||||
|  | ||||
|     def test_stage_str(self): | ||||
|         """Test string representation of stage""" | ||||
|         self.assertEqual(str(self.stage), f"Email Authenticator Stage {self.stage.name}") | ||||
|  | ||||
|     def test_token_lifecycle(self): | ||||
|         """Test token generation, validation and expiry""" | ||||
|         # Initially no token | ||||
|         self.assertIsNone(self.device.token) | ||||
|  | ||||
|         # Generate token | ||||
|         self.device.generate_token() | ||||
|         token = self.device.token | ||||
|         self.assertIsNotNone(token) | ||||
|         self.assertIsNotNone(self.device.valid_until) | ||||
|         self.assertTrue(self.device.valid_until > now()) | ||||
|  | ||||
|         # Verify invalid token | ||||
|         self.assertFalse(self.device.verify_token("000000")) | ||||
|  | ||||
|         # Verify correct token (should clear token after verification) | ||||
|         self.assertTrue(self.device.verify_token(token)) | ||||
|         self.assertIsNone(self.device.token) | ||||
|  | ||||
|     def test_stage_no_prefill(self): | ||||
|         """Test stage without prefilled email""" | ||||
|         self.client.force_login(self.user_noemail) | ||||
|         with patch( | ||||
|             "authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class", | ||||
|             PropertyMock(return_value=EmailBackend), | ||||
|         ): | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             ) | ||||
|             self.assertStageResponse( | ||||
|                 response, | ||||
|                 self.flow, | ||||
|                 self.user_noemail, | ||||
|                 component="ak-stage-authenticator-email", | ||||
|                 email_required=True, | ||||
|             ) | ||||
|  | ||||
|     def test_stage_submit(self): | ||||
|         """Test stage email submission""" | ||||
|         # Initialize the flow | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|         ) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             self.flow, | ||||
|             self.user, | ||||
|             component="ak-stage-authenticator-email", | ||||
|             email_required=False, | ||||
|         ) | ||||
|  | ||||
|         # Test email submission with locmem backend | ||||
|         def mock_send_mails(stage, *messages): | ||||
|             """Mock send_mails to send directly""" | ||||
|             for message in messages: | ||||
|                 message.send() | ||||
|  | ||||
|         with ( | ||||
|             patch( | ||||
|                 "authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class", | ||||
|                 return_value=EmailBackend, | ||||
|             ), | ||||
|             patch( | ||||
|                 "authentik.stages.authenticator_email.stage.send_mails", | ||||
|                 side_effect=mock_send_mails, | ||||
|             ), | ||||
|         ): | ||||
|             response = self.client.post( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|                 data={"component": "ak-stage-authenticator-email", "email": "test@example.com"}, | ||||
|             ) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertEqual(len(mail.outbox), 1) | ||||
|             sent_mail = mail.outbox[0] | ||||
|             self.assertEqual(sent_mail.subject, self.stage.subject) | ||||
|             self.assertEqual(sent_mail.to, [f"{self.user} <test@example.com>"]) | ||||
|             # Get from_address from global email config to test if global settings are being used | ||||
|             from_address_global = CONFIG.get("email.from") | ||||
|             self.assertEqual(sent_mail.from_email, from_address_global) | ||||
|  | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             self.flow, | ||||
|             self.user, | ||||
|             component="ak-stage-authenticator-email", | ||||
|             response_errors={}, | ||||
|             email_required=False, | ||||
|         ) | ||||
|  | ||||
|     def test_email_template(self): | ||||
|         """Test email template rendering""" | ||||
|         self.device.generate_token() | ||||
|         message = self.device._compose_email() | ||||
|  | ||||
|         self.assertIsInstance(message, TemplateEmailMessage) | ||||
|         self.assertEqual(message.subject, self.stage.subject) | ||||
|         self.assertEqual(message.to, [f"{self.user.name} <{self.device.email}>"]) | ||||
|         self.assertTrue(self.device.token in message.body) | ||||
|  | ||||
|     def test_duplicate_email(self): | ||||
|         """Test attempting to use same email twice""" | ||||
|         email = "test2@authentik.local" | ||||
|         # First device | ||||
|         EmailDevice.objects.create( | ||||
|             user=self.user, | ||||
|             stage=self.stage, | ||||
|             email=email, | ||||
|         ) | ||||
|         # Attempt to create second device with same email | ||||
|         with self.assertRaises(IntegrityError): | ||||
|             EmailDevice.objects.create( | ||||
|                 user=self.user, | ||||
|                 stage=self.stage, | ||||
|                 email=email, | ||||
|             ) | ||||
|  | ||||
|     def test_token_expiry(self): | ||||
|         """Test token expiration behavior""" | ||||
|         self.device.generate_token() | ||||
|         token = self.device.token | ||||
|         # Set token as expired | ||||
|         self.device.valid_until = now() - timedelta(minutes=1) | ||||
|         self.device.save() | ||||
|         # Verify expired token fails | ||||
|         self.assertFalse(self.device.verify_token(token)) | ||||
|  | ||||
|     def test_template_errors(self): | ||||
|         """Test handling of template errors""" | ||||
|         self.stage.template = "{% invalid template %}" | ||||
|         with self.assertRaises(TemplateDoesNotExist): | ||||
|             self.stage.send(self.device) | ||||
|  | ||||
|     def test_challenge_response_validation(self): | ||||
|         """Test challenge response validation""" | ||||
|         # Initialize the flow | ||||
|         self.client.force_login(self.user_noemail) | ||||
|         with patch( | ||||
|             "authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class", | ||||
|             PropertyMock(return_value=EmailBackend), | ||||
|         ): | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             ) | ||||
|  | ||||
|             # Test missing code and email | ||||
|             response = self.client.post( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|                 data={"component": "ak-stage-authenticator-email"}, | ||||
|             ) | ||||
|             self.assertIn("email required", str(response.content)) | ||||
|  | ||||
|             # Test invalid code | ||||
|             response = self.client.post( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|                 data={"component": "ak-stage-authenticator-email", "code": "000000"}, | ||||
|             ) | ||||
|             self.assertIn("Code does not match", str(response.content)) | ||||
|  | ||||
|             # Test valid code | ||||
|             self.client.force_login(self.user) | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             ) | ||||
|             device = self.device | ||||
|             token = device.token | ||||
|             response = self.client.post( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|                 data={"component": "ak-stage-authenticator-email", "code": token}, | ||||
|             ) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertTrue(device.confirmed) | ||||
|  | ||||
|     def test_challenge_generation(self): | ||||
|         """Test challenge generation""" | ||||
|         # Test with masked email | ||||
|         with patch( | ||||
|             "authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class", | ||||
|             PropertyMock(return_value=EmailBackend), | ||||
|         ): | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             ) | ||||
|             self.assertStageResponse( | ||||
|                 response, | ||||
|                 self.flow, | ||||
|                 self.user, | ||||
|                 component="ak-stage-authenticator-email", | ||||
|                 email_required=False, | ||||
|             ) | ||||
|             masked_email = mask_email(self.user.email) | ||||
|             self.assertEqual(masked_email, response.json()["email"]) | ||||
|  | ||||
|             # Test without email | ||||
|             self.client.force_login(self.user_noemail) | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             ) | ||||
|             self.assertStageResponse( | ||||
|                 response, | ||||
|                 self.flow, | ||||
|                 self.user_noemail, | ||||
|                 component="ak-stage-authenticator-email", | ||||
|                 email_required=True, | ||||
|             ) | ||||
|             self.assertIsNone(response.json()["email"]) | ||||
|  | ||||
|     def test_session_management(self): | ||||
|         """Test session device management""" | ||||
|         # Test device creation in session | ||||
|         with patch( | ||||
|             "authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class", | ||||
|             PropertyMock(return_value=EmailBackend), | ||||
|         ): | ||||
|             # Delete any existing devices for this test | ||||
|             EmailDevice.objects.filter(user=self.user).delete() | ||||
|  | ||||
|             response = self.client.get( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             ) | ||||
|             self.assertIn(SESSION_KEY_EMAIL_DEVICE, self.client.session) | ||||
|             device = self.client.session[SESSION_KEY_EMAIL_DEVICE] | ||||
|             self.assertIsInstance(device, EmailDevice) | ||||
|             self.assertFalse(device.confirmed) | ||||
|             self.assertEqual(device.user, self.user) | ||||
|  | ||||
|             # Test device confirmation and cleanup | ||||
|             device.confirmed = True | ||||
|             device.email = "new_test@authentik.local"  # Use a different email | ||||
|             self.client.session[SESSION_KEY_EMAIL_DEVICE] = device | ||||
|             self.client.session.save() | ||||
|             response = self.client.post( | ||||
|                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|                 data={"component": "ak-stage-authenticator-email", "code": device.token}, | ||||
|             ) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertTrue(device.confirmed) | ||||
|             # Session key should be removed after device is saved | ||||
|             device.save() | ||||
|             self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, self.client.session) | ||||
|  | ||||
|     def test_model_properties_and_methods(self): | ||||
|         """Test model properties""" | ||||
|         device = self.device | ||||
|         stage = self.stage | ||||
|  | ||||
|         self.assertEqual(stage.serializer, AuthenticatorEmailStageSerializer) | ||||
|         self.assertIsInstance(stage.backend, EmailBackend) | ||||
|         self.assertEqual(device.serializer, EmailDeviceSerializer) | ||||
|  | ||||
|         # Test AuthenticatorEmailStage send method | ||||
|         with patch( | ||||
|             "authentik.stages.authenticator_email.models.AuthenticatorEmailStage.backend_class", | ||||
|             return_value=EmailBackend, | ||||
|         ): | ||||
|             self.device.generate_token() | ||||
|             # Test EmailDevice _compose_email method | ||||
|             message = self.device._compose_email() | ||||
|             self.assertIsInstance(message, TemplateEmailMessage) | ||||
|             self.assertEqual(message.subject, self.stage.subject) | ||||
|             self.assertEqual(message.to, [f"{self.user.name} <{self.device.email}>"]) | ||||
|             self.assertTrue(self.device.token in message.body) | ||||
|             # Test AuthenticatorEmailStage send method | ||||
|             self.stage.send(device) | ||||
|  | ||||
|     def test_email_tasks(self): | ||||
|  | ||||
|         email_send_mock = MagicMock() | ||||
|         with patch( | ||||
|             "authentik.stages.email.tasks.send_mails", | ||||
|             email_send_mock, | ||||
|         ): | ||||
|             # Test AuthenticatorEmailStage send method | ||||
|             self.stage.send(self.device) | ||||
|             email_send_mock.assert_called_once() | ||||
| @ -1,17 +0,0 @@ | ||||
| """API URLs""" | ||||
|  | ||||
| from authentik.stages.authenticator_email.api import ( | ||||
|     AuthenticatorEmailStageViewSet, | ||||
|     EmailAdminDeviceViewSet, | ||||
|     EmailDeviceViewSet, | ||||
| ) | ||||
|  | ||||
| api_urlpatterns = [ | ||||
|     ("authenticators/email", EmailDeviceViewSet), | ||||
|     ( | ||||
|         "authenticators/admin/email", | ||||
|         EmailAdminDeviceViewSet, | ||||
|         "admin-emaildevice", | ||||
|     ), | ||||
|     ("stages/authenticator/email", AuthenticatorEmailStageViewSet), | ||||
| ] | ||||
| @ -26,13 +26,10 @@ from authentik.events.middleware import audit_ignore | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE | ||||
| from authentik.lib.utils.email import mask_email | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.root.middleware import ClientIPMiddleware | ||||
| from authentik.stages.authenticator import match_token | ||||
| from authentik.stages.authenticator.models import Device | ||||
| from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | ||||
| from authentik.stages.authenticator_email.models import EmailDevice | ||||
| from authentik.stages.authenticator_sms.models import SMSDevice | ||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | ||||
| @ -57,8 +54,6 @@ def get_challenge_for_device( | ||||
|     """Generate challenge for a single device""" | ||||
|     if isinstance(device, WebAuthnDevice): | ||||
|         return get_webauthn_challenge(request, stage, device) | ||||
|     if isinstance(device, EmailDevice): | ||||
|         return {"email": mask_email(device.email)} | ||||
|     # Code-based challenges have no hints | ||||
|     return {} | ||||
|  | ||||
| @ -108,8 +103,6 @@ def select_challenge(request: HttpRequest, device: Device): | ||||
|     """Callback when the user selected a challenge in the frontend.""" | ||||
|     if isinstance(device, SMSDevice): | ||||
|         select_challenge_sms(request, device) | ||||
|     elif isinstance(device, EmailDevice): | ||||
|         select_challenge_email(request, device) | ||||
|  | ||||
|  | ||||
| def select_challenge_sms(request: HttpRequest, device: SMSDevice): | ||||
| @ -118,13 +111,6 @@ def select_challenge_sms(request: HttpRequest, device: SMSDevice): | ||||
|     device.stage.send(device.token, device) | ||||
|  | ||||
|  | ||||
| def select_challenge_email(request: HttpRequest, device: EmailDevice): | ||||
|     """Send Email""" | ||||
|     valid_secs: int = timedelta_from_string(device.stage.token_expiry).total_seconds() | ||||
|     device.generate_token(valid_secs=valid_secs) | ||||
|     device.stage.send(device) | ||||
|  | ||||
|  | ||||
| def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Device: | ||||
|     """Validate code-based challenges. We test against every device, on purpose, as | ||||
|     the user mustn't choose between totp and static devices.""" | ||||
|  | ||||
| @ -1,37 +0,0 @@ | ||||
| # Generated by Django 5.0.10 on 2025-01-16 02:48 | ||||
|  | ||||
| import authentik.stages.authenticator_validate.models | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ( | ||||
|             "authentik_stages_authenticator_validate", | ||||
|             "0013_authenticatorvalidatestage_webauthn_allowed_device_types", | ||||
|         ), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="authenticatorvalidatestage", | ||||
|             name="device_classes", | ||||
|             field=django.contrib.postgres.fields.ArrayField( | ||||
|                 base_field=models.TextField( | ||||
|                     choices=[ | ||||
|                         ("static", "Static"), | ||||
|                         ("totp", "TOTP"), | ||||
|                         ("webauthn", "WebAuthn"), | ||||
|                         ("duo", "Duo"), | ||||
|                         ("sms", "SMS"), | ||||
|                         ("email", "Email"), | ||||
|                     ] | ||||
|                 ), | ||||
|                 default=authentik.stages.authenticator_validate.models.default_device_classes, | ||||
|                 help_text="Device classes which can be used to authenticate", | ||||
|                 size=None, | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -20,7 +20,6 @@ class DeviceClasses(models.TextChoices): | ||||
|     WEBAUTHN = "webauthn", _("WebAuthn") | ||||
|     DUO = "duo", _("Duo") | ||||
|     SMS = "sms", _("SMS") | ||||
|     EMAIL = "email", _("Email") | ||||
|  | ||||
|  | ||||
| def default_device_classes() -> list: | ||||
| @ -31,7 +30,6 @@ def default_device_classes() -> list: | ||||
|         DeviceClasses.WEBAUTHN, | ||||
|         DeviceClasses.DUO, | ||||
|         DeviceClasses.SMS, | ||||
|         DeviceClasses.EMAIL, | ||||
|     ] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -23,7 +23,6 @@ from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.stages.authenticator import devices_for_user | ||||
| from authentik.stages.authenticator.models import Device | ||||
| from authentik.stages.authenticator_email.models import EmailDevice | ||||
| from authentik.stages.authenticator_sms.models import SMSDevice | ||||
| from authentik.stages.authenticator_validate.challenge import ( | ||||
|     DeviceChallenge, | ||||
| @ -85,9 +84,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|  | ||||
|     def validate_code(self, code: str) -> str: | ||||
|         """Validate code-based response, raise error if code isn't allowed""" | ||||
|         self._challenge_allowed( | ||||
|             [DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS, DeviceClasses.EMAIL] | ||||
|         ) | ||||
|         self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC, DeviceClasses.SMS]) | ||||
|         self.device = validate_challenge_code(code, self.stage, self.stage.get_pending_user()) | ||||
|         return code | ||||
|  | ||||
| @ -120,17 +117,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|         if not allowed: | ||||
|             raise ValidationError("invalid challenge selected") | ||||
|  | ||||
|         device_class = challenge.get("device_class", "") | ||||
|         if device_class == "sms": | ||||
|             devices = SMSDevice.objects.filter(pk=int(challenge.get("device_uid", "0"))) | ||||
|             if not devices.exists(): | ||||
|                 raise ValidationError("invalid challenge selected") | ||||
|             select_challenge(self.stage.request, devices.first()) | ||||
|         elif device_class == "email": | ||||
|             devices = EmailDevice.objects.filter(pk=int(challenge.get("device_uid", "0"))) | ||||
|             if not devices.exists(): | ||||
|                 raise ValidationError("invalid challenge selected") | ||||
|             select_challenge(self.stage.request, devices.first()) | ||||
|         if challenge.get("device_class", "") != "sms": | ||||
|             return challenge | ||||
|         devices = SMSDevice.objects.filter(pk=int(challenge.get("device_uid", "0"))) | ||||
|         if not devices.exists(): | ||||
|             raise ValidationError("invalid challenge selected") | ||||
|         select_challenge(self.stage.request, devices.first()) | ||||
|         return challenge | ||||
|  | ||||
|     def validate_selected_stage(self, stage_pk: str) -> str: | ||||
|  | ||||
| @ -1,183 +0,0 @@ | ||||
| """Test validator stage for Email devices""" | ||||
|  | ||||
| from django.test.client import RequestFactory | ||||
| from django.urls.base import reverse | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.flows.models import FlowStageBinding, NotConfiguredAction | ||||
| from authentik.flows.tests import FlowTestCase | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.lib.utils.email import mask_email | ||||
| from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice | ||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | ||||
|  | ||||
|  | ||||
| class AuthenticatorValidateStageEmailTests(FlowTestCase): | ||||
|     """Test validator stage for Email devices""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.user = create_test_admin_user() | ||||
|         self.request_factory = RequestFactory() | ||||
|         # Create email authenticator stage | ||||
|         self.stage = AuthenticatorEmailStage.objects.create( | ||||
|             name="email-authenticator", | ||||
|             use_global_settings=True, | ||||
|             from_address="test@authentik.local", | ||||
|         ) | ||||
|         # Create identification stage | ||||
|         self.ident_stage = IdentificationStage.objects.create( | ||||
|             name=generate_id(), | ||||
|             user_fields=[UserFields.USERNAME], | ||||
|         ) | ||||
|         # Create validation stage | ||||
|         self.validate_stage = AuthenticatorValidateStage.objects.create( | ||||
|             name=generate_id(), | ||||
|             device_classes=[DeviceClasses.EMAIL], | ||||
|         ) | ||||
|         # Create flow with both stages | ||||
|         self.flow = create_test_flow() | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.ident_stage, order=0) | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.validate_stage, order=1) | ||||
|  | ||||
|     def _identify_user(self): | ||||
|         """Helper to identify user in flow""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             {"uid_field": self.user.username}, | ||||
|             follow=True, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         return response | ||||
|  | ||||
|     def _send_challenge(self, device): | ||||
|         """Helper to send challenge for device""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             { | ||||
|                 "component": "ak-stage-authenticator-validate", | ||||
|                 "selected_challenge": { | ||||
|                     "device_class": "email", | ||||
|                     "device_uid": str(device.pk), | ||||
|                     "challenge": {}, | ||||
|                     "last_used": device.last_used.isoformat() if device.last_used else None, | ||||
|                 }, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         return response | ||||
|  | ||||
|     def test_happy_path(self): | ||||
|         """Test validator stage with valid code""" | ||||
|         # Create a device for our user | ||||
|         device = EmailDevice.objects.create( | ||||
|             user=self.user, | ||||
|             confirmed=True, | ||||
|             stage=self.stage, | ||||
|             email="xx@0.co", | ||||
|         )  # Short email for testing purposes | ||||
|  | ||||
|         # First identify the user | ||||
|         self._identify_user() | ||||
|  | ||||
|         # Send the challenge | ||||
|         response = self._send_challenge(device) | ||||
|         response_data = self.assertStageResponse( | ||||
|             response, | ||||
|             flow=self.flow, | ||||
|             component="ak-stage-authenticator-validate", | ||||
|         ) | ||||
|  | ||||
|         # Get the device challenge from the response and verify it matches | ||||
|         device_challenge = response_data["device_challenges"][0] | ||||
|         self.assertEqual(device_challenge["device_class"], "email") | ||||
|         self.assertEqual(device_challenge["device_uid"], str(device.pk)) | ||||
|         self.assertEqual(device_challenge["challenge"], {"email": mask_email(device.email)}) | ||||
|  | ||||
|         # Generate a token for the device | ||||
|         device.generate_token() | ||||
|  | ||||
|         # Submit the valid code | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             {"component": "ak-stage-authenticator-validate", "code": device.token}, | ||||
|         ) | ||||
|         # Should redirect to root since this is the last stage | ||||
|         self.assertStageRedirects(response, "/") | ||||
|  | ||||
|     def test_no_device(self): | ||||
|         """Test validator stage without configured device""" | ||||
|         configuration_stage = AuthenticatorEmailStage.objects.create( | ||||
|             name=generate_id(), | ||||
|             use_global_settings=True, | ||||
|             from_address="test@authentik.local", | ||||
|         ) | ||||
|         stage = AuthenticatorValidateStage.objects.create( | ||||
|             name=generate_id(), | ||||
|             not_configured_action=NotConfiguredAction.CONFIGURE, | ||||
|             device_classes=[DeviceClasses.EMAIL], | ||||
|         ) | ||||
|         stage.configuration_stages.set([configuration_stage]) | ||||
|         flow = create_test_flow() | ||||
|         FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|  | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|             {"component": "ak-stage-authenticator-validate"}, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         response_data = self.assertStageResponse( | ||||
|             response, | ||||
|             flow=flow, | ||||
|             component="ak-stage-authenticator-validate", | ||||
|         ) | ||||
|         self.assertEqual(response_data["configuration_stages"], []) | ||||
|         self.assertEqual(response_data["device_challenges"], []) | ||||
|         self.assertEqual( | ||||
|             response_data["response_errors"], | ||||
|             {"non_field_errors": [{"code": "invalid", "string": "Empty response"}]}, | ||||
|         ) | ||||
|  | ||||
|     def test_invalid_code(self): | ||||
|         """Test validator stage with invalid code""" | ||||
|         # Create a device for our user | ||||
|         device = EmailDevice.objects.create( | ||||
|             user=self.user, | ||||
|             confirmed=True, | ||||
|             stage=self.stage, | ||||
|             email="test@authentik.local", | ||||
|         ) | ||||
|  | ||||
|         # First identify the user | ||||
|         self._identify_user() | ||||
|  | ||||
|         # Send the challenge | ||||
|         self._send_challenge(device) | ||||
|  | ||||
|         # Generate a token for the device | ||||
|         device.generate_token() | ||||
|  | ||||
|         # Try invalid code and verify error message | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||
|             {"component": "ak-stage-authenticator-validate", "code": "invalid"}, | ||||
|         ) | ||||
|         response_data = self.assertStageResponse( | ||||
|             response, | ||||
|             flow=self.flow, | ||||
|             component="ak-stage-authenticator-validate", | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             response_data["response_errors"], | ||||
|             { | ||||
|                 "code": [ | ||||
|                     { | ||||
|                         "code": "invalid", | ||||
|                         "string": ( | ||||
|                             "Invalid Token. Please ensure the time on your device " | ||||
|                             "is accurate and try again." | ||||
|                         ), | ||||
|                     } | ||||
|                 ], | ||||
|             }, | ||||
|         ) | ||||
| @ -13,28 +13,17 @@ from structlog.stdlib import get_logger | ||||
| from authentik.events.models import Event, EventAction, TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.stages.authenticator_email.models import AuthenticatorEmailStage | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.utils import logo_data | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def send_mails( | ||||
|     stage: EmailStage | AuthenticatorEmailStage, *messages: list[EmailMultiAlternatives] | ||||
| ): | ||||
|     """Wrapper to convert EmailMessage to dict and send it from worker | ||||
|  | ||||
|     Args: | ||||
|         stage: Either an EmailStage or AuthenticatorEmailStage instance | ||||
|         messages: List of email messages to send | ||||
|     Returns: | ||||
|         Celery group promise for the email sending tasks | ||||
|     """ | ||||
| def send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]): | ||||
|     """Wrapper to convert EmailMessage to dict and send it from worker""" | ||||
|     tasks = [] | ||||
|     stage_class = stage.__class__ | ||||
|     for message in messages: | ||||
|         tasks.append(send_mail.s(message.__dict__, stage_class, str(stage.pk))) | ||||
|         tasks.append(send_mail.s(message.__dict__, str(stage.pk))) | ||||
|     lazy_group = group(*tasks) | ||||
|     promise = lazy_group() | ||||
|     return promise | ||||
| @ -58,28 +47,23 @@ def get_email_body(email: EmailMultiAlternatives) -> str: | ||||
|     retry_backoff=True, | ||||
|     base=SystemTask, | ||||
| ) | ||||
| def send_mail( | ||||
|     self: SystemTask, | ||||
|     message: dict[Any, Any], | ||||
|     stage_class: EmailStage | AuthenticatorEmailStage = EmailStage, | ||||
|     email_stage_pk: str | None = None, | ||||
| ): | ||||
| def send_mail(self: SystemTask, message: dict[Any, Any], email_stage_pk: str | None = None): | ||||
|     """Send Email for Email Stage. Retries are scheduled automatically.""" | ||||
|     self.save_on_success = False | ||||
|     message_id = make_msgid(domain=DNS_NAME) | ||||
|     self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_"))) | ||||
|     try: | ||||
|         if not email_stage_pk: | ||||
|             stage: EmailStage | AuthenticatorEmailStage = stage_class(use_global_settings=True) | ||||
|             stage: EmailStage = EmailStage(use_global_settings=True) | ||||
|         else: | ||||
|             stages = stage_class.objects.filter(pk=email_stage_pk) | ||||
|             stages = EmailStage.objects.filter(pk=email_stage_pk) | ||||
|             if not stages.exists(): | ||||
|                 self.set_status( | ||||
|                     TaskStatus.WARNING, | ||||
|                     "Email stage does not exist anymore. Discarding message.", | ||||
|                 ) | ||||
|                 return | ||||
|             stage: EmailStage | AuthenticatorEmailStage = stages.first() | ||||
|             stage: EmailStage = stages.first() | ||||
|         try: | ||||
|             backend = stage.backend | ||||
|         except ValueError as exc: | ||||
|  | ||||
| @ -1,30 +0,0 @@ | ||||
| version: 1 | ||||
| metadata: | ||||
|   labels: | ||||
|     blueprints.goauthentik.io/instantiate: "false" | ||||
|   name: Example - Email MFA setup flow | ||||
| entries: | ||||
| - attrs: | ||||
|     designation: stage_configuration | ||||
|     name: Default Email Authenticator Flow | ||||
|     title: Setup Email Two-Factor Authentication | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-authenticator-email-setup | ||||
|   model: authentik_flows.flow | ||||
|   id: flow | ||||
| - attrs: | ||||
|     configure_flow: !KeyOf flow | ||||
|     friendly_name: Email Authenticator | ||||
|     use_global_settings: true | ||||
|     token_expiry: minutes=30 | ||||
|     subject: authentik Sign-in code | ||||
|   identifiers: | ||||
|     name: default-authenticator-email-setup | ||||
|   id: default-authenticator-email-setup | ||||
|   model: authentik_stages_authenticator_email.authenticatoremailstage | ||||
| - identifiers: | ||||
|     order: 0 | ||||
|     stage: !KeyOf default-authenticator-email-setup | ||||
|     target: !KeyOf flow | ||||
|   model: authentik_flows.flowstagebinding | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -10,7 +10,6 @@ import ( | ||||
|  | ||||
| 	"goauthentik.io/internal/common" | ||||
| 	"goauthentik.io/internal/config" | ||||
| 	"goauthentik.io/internal/constants" | ||||
| 	"goauthentik.io/internal/debug" | ||||
| 	"goauthentik.io/internal/outpost/ak" | ||||
| 	"goauthentik.io/internal/outpost/ak/healthcheck" | ||||
| @ -25,8 +24,7 @@ Required environment variables: | ||||
| - AUTHENTIK_INSECURE: Skip SSL Certificate verification` | ||||
|  | ||||
| var rootCmd = &cobra.Command{ | ||||
| 	Long:    helpMessage, | ||||
| 	Version: constants.FullVersion(), | ||||
| 	Long: helpMessage, | ||||
| 	PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 		log.SetFormatter(&log.JSONFormatter{ | ||||
|  | ||||
| @ -10,7 +10,6 @@ import ( | ||||
|  | ||||
| 	"goauthentik.io/internal/common" | ||||
| 	"goauthentik.io/internal/config" | ||||
| 	"goauthentik.io/internal/constants" | ||||
| 	"goauthentik.io/internal/debug" | ||||
| 	"goauthentik.io/internal/outpost/ak" | ||||
| 	"goauthentik.io/internal/outpost/ak/healthcheck" | ||||
| @ -28,8 +27,7 @@ Optionally, you can set these: | ||||
| - AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST` | ||||
|  | ||||
| var rootCmd = &cobra.Command{ | ||||
| 	Long:    helpMessage, | ||||
| 	Version: constants.FullVersion(), | ||||
| 	Long: helpMessage, | ||||
| 	PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 		log.SetFormatter(&log.JSONFormatter{ | ||||
|  | ||||
| @ -9,7 +9,6 @@ import ( | ||||
| 	"github.com/spf13/cobra" | ||||
|  | ||||
| 	"goauthentik.io/internal/common" | ||||
| 	"goauthentik.io/internal/constants" | ||||
| 	"goauthentik.io/internal/debug" | ||||
| 	"goauthentik.io/internal/outpost/ak" | ||||
| 	"goauthentik.io/internal/outpost/ak/healthcheck" | ||||
| @ -24,8 +23,7 @@ Required environment variables: | ||||
| - AUTHENTIK_INSECURE: Skip SSL Certificate verification` | ||||
|  | ||||
| var rootCmd = &cobra.Command{ | ||||
| 	Long:    helpMessage, | ||||
| 	Version: constants.FullVersion(), | ||||
| 	Long: helpMessage, | ||||
| 	PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 		log.SetFormatter(&log.JSONFormatter{ | ||||
|  | ||||
| @ -9,7 +9,6 @@ import ( | ||||
| 	"github.com/spf13/cobra" | ||||
|  | ||||
| 	"goauthentik.io/internal/common" | ||||
| 	"goauthentik.io/internal/constants" | ||||
| 	"goauthentik.io/internal/debug" | ||||
| 	"goauthentik.io/internal/outpost/ak" | ||||
| 	"goauthentik.io/internal/outpost/ak/healthcheck" | ||||
| @ -24,8 +23,7 @@ Required environment variables: | ||||
| - AUTHENTIK_INSECURE: Skip SSL Certificate verification` | ||||
|  | ||||
| var rootCmd = &cobra.Command{ | ||||
| 	Long:    helpMessage, | ||||
| 	Version: constants.FullVersion(), | ||||
| 	Long: helpMessage, | ||||
| 	PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||||
| 		log.SetLevel(log.DebugLevel) | ||||
| 		log.SetFormatter(&log.JSONFormatter{ | ||||
|  | ||||
| @ -31,7 +31,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -54,7 +54,7 @@ services: | ||||
|       redis: | ||||
|         condition: service_healthy | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							| @ -26,10 +26,10 @@ require ( | ||||
| 	github.com/redis/go-redis/v9 v9.7.0 | ||||
| 	github.com/sethvargo/go-envconfig v1.1.1 | ||||
| 	github.com/sirupsen/logrus v1.9.3 | ||||
| 	github.com/spf13/cobra v1.9.1 | ||||
| 	github.com/spf13/cobra v1.8.1 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/wwt/guac v1.3.2 | ||||
| 	goauthentik.io/api/v3 v3.2024123.6 | ||||
| 	goauthentik.io/api/v3 v3.2024123.4 | ||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||
| 	golang.org/x/oauth2 v0.26.0 | ||||
| 	golang.org/x/sync v0.11.0 | ||||
| @ -71,7 +71,7 @@ require ( | ||||
| 	github.com/prometheus/client_model v0.6.1 // indirect | ||||
| 	github.com/prometheus/common v0.55.0 // indirect | ||||
| 	github.com/prometheus/procfs v0.15.1 // indirect | ||||
| 	github.com/spf13/pflag v1.0.6 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 // indirect | ||||
| 	go.mongodb.org/mongo-driver v1.14.0 // indirect | ||||
| 	go.opentelemetry.io/otel v1.24.0 // indirect | ||||
| 	go.opentelemetry.io/otel/metric v1.24.0 // indirect | ||||
|  | ||||
							
								
								
									
										14
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								go.sum
									
									
									
									
									
								
							| @ -57,7 +57,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk | ||||
| github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= | ||||
| github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo= | ||||
| github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| @ -259,10 +259,10 @@ github.com/sethvargo/go-envconfig v1.1.1/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/ | ||||
| github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | ||||
| github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||
| github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||
| github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= | ||||
| github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= | ||||
| github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= | ||||
| github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= | ||||
| github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= | ||||
| github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | ||||
| github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| @ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | ||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||
| goauthentik.io/api/v3 v3.2024123.6 h1:AGOCa7Fc/9eONCPEW4sEhTiyEBvxN57Lfqz1zm6Gy98= | ||||
| goauthentik.io/api/v3 v3.2024123.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| goauthentik.io/api/v3 v3.2024123.4 h1:JYLsUjkJ7kT+jHO72DyFTXFwKEGAcOOlLh36SRG9BDw= | ||||
| goauthentik.io/api/v3 v3.2024123.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
|  | ||||
| @ -29,4 +29,4 @@ func UserAgent() string { | ||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2025.2.0" | ||||
| const VERSION = "2024.12.3" | ||||
|  | ||||
							
								
								
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -9,7 +9,7 @@ | ||||
|             "version": "0.0.0", | ||||
|             "license": "MIT", | ||||
|             "devDependencies": { | ||||
|                 "aws-cdk": "^2.179.0", | ||||
|                 "aws-cdk": "^2.178.2", | ||||
|                 "cross-env": "^7.0.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -17,9 +17,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/aws-cdk": { | ||||
|             "version": "2.179.0", | ||||
|             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.179.0.tgz", | ||||
|             "integrity": "sha512-aA2+8S2g4UBQHkUEt0mYd16VLt/ucR+QfyUJi34LDKRAhOCNDjPCZ4z9z/JEDyuni0BdzsYA55pnpDN9tMULpA==", | ||||
|             "version": "2.178.2", | ||||
|             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.178.2.tgz", | ||||
|             "integrity": "sha512-ojMCMnBGinvDUD6+BOOlUOB9pjsYXoQdFVbf4bvi3dy3nwn557r0j6qDUcJMeikzPJ6YWzfAdL0fYxBZg4xcOg==", | ||||
|             "dev": true, | ||||
|             "license": "Apache-2.0", | ||||
|             "bin": { | ||||
|  | ||||
| @ -10,7 +10,7 @@ | ||||
|         "node": ">=20" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "aws-cdk": "^2.179.0", | ||||
|         "aws-cdk": "^2.178.2", | ||||
|         "cross-env": "^7.0.3" | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -26,7 +26,7 @@ Parameters: | ||||
|     Description: authentik Docker image | ||||
|   AuthentikVersion: | ||||
|     Type: String | ||||
|     Default: 2025.2.0 | ||||
|     Default: 2024.12.3 | ||||
|     Description: authentik Docker image tag | ||||
|   AuthentikServerCPU: | ||||
|     Type: Number | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -8,7 +8,7 @@ | ||||
| # 刘松, 2022 | ||||
| # Tianhao Chai <cth451@gmail.com>, 2024 | ||||
| # Jens L. <jens@goauthentik.io>, 2024 | ||||
| # deluxghost, 2025 | ||||
| # deluxghost, 2024 | ||||
| #  | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| @ -17,7 +17,7 @@ msgstr "" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-02-14 14:49+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: deluxghost, 2025\n" | ||||
| "Last-Translator: deluxghost, 2024\n" | ||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| @ -568,39 +568,39 @@ msgstr "签名密钥" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Key used to sign the SSF Events." | ||||
| msgstr "用于签名 SSF 时间的密钥。" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Provider" | ||||
| msgstr "Shared Signals Framework 提供程序" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Providers" | ||||
| msgstr "Shared Signals Framework 提供程序" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Add stream to SSF provider" | ||||
| msgstr "向 SSF 提供程序添加流" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream" | ||||
| msgstr "SSF 流" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Streams" | ||||
| msgstr "SSF 流" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Event" | ||||
| msgstr "SSF 流事件" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Events" | ||||
| msgstr "SSF 流事件" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/tasks.py | ||||
| msgid "Failed to send request" | ||||
| msgstr "发送请求失败" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py | ||||
| msgid "Endpoint Authenticator Google Device Trust Connector Stage" | ||||
| @ -878,7 +878,7 @@ msgstr "在流程规划过程中评估策略。" | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "Evaluate policies when the Stage is presented to the user." | ||||
| msgstr "在阶段呈现给用户时评估策略。" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "" | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
| # Chen Zhikai, 2022 | ||||
| # 刘松, 2022 | ||||
| # Jens L. <jens@goauthentik.io>, 2024 | ||||
| # deluxghost, 2025 | ||||
| # deluxghost, 2024 | ||||
| #  | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| @ -16,7 +16,7 @@ msgstr "" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-02-14 14:49+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: deluxghost, 2025\n" | ||||
| "Last-Translator: deluxghost, 2024\n" | ||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| @ -567,39 +567,39 @@ msgstr "签名密钥" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Key used to sign the SSF Events." | ||||
| msgstr "用于签名 SSF 时间的密钥。" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Provider" | ||||
| msgstr "Shared Signals Framework 提供程序" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Providers" | ||||
| msgstr "Shared Signals Framework 提供程序" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Add stream to SSF provider" | ||||
| msgstr "向 SSF 提供程序添加流" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream" | ||||
| msgstr "SSF 流" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Streams" | ||||
| msgstr "SSF 流" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Event" | ||||
| msgstr "SSF 流事件" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Events" | ||||
| msgstr "SSF 流事件" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/tasks.py | ||||
| msgid "Failed to send request" | ||||
| msgstr "发送请求失败" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py | ||||
| msgid "Endpoint Authenticator Google Device Trust Connector Stage" | ||||
| @ -877,7 +877,7 @@ msgstr "在流程规划过程中评估策略。" | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "Evaluate policies when the Stage is presented to the user." | ||||
| msgstr "在阶段呈现给用户时评估策略。" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "" | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| { | ||||
|     "name": "@goauthentik/authentik", | ||||
|     "version": "2025.2.0", | ||||
|     "version": "2024.12.3", | ||||
|     "private": true | ||||
| } | ||||
|  | ||||
							
								
								
									
										88
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										88
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -358,6 +358,22 @@ jsii = ">=1.105.0,<2.0.0" | ||||
| publication = ">=0.0.3" | ||||
| typeguard = ">=2.13.3,<4.3.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "aws-cdk-asset-kubectl-v20" | ||||
| version = "2.1.3" | ||||
| description = "A Lambda Layer that contains kubectl v1.20" | ||||
| optional = false | ||||
| python-versions = "~=3.8" | ||||
| files = [ | ||||
|     {file = "aws_cdk.asset_kubectl_v20-2.1.3-py3-none-any.whl", hash = "sha256:d5612e5bd03c215a28ce53193b1144ecf4e93b3b6779563c046a8a74d83a3979"}, | ||||
|     {file = "aws_cdk_asset_kubectl_v20-2.1.3.tar.gz", hash = "sha256:237cd8530d9e8be0bbc7159af927dbb6b7f91bf3f4099c8ef4d9a213b34264be"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| jsii = ">=1.103.1,<2.0.0" | ||||
| publication = ">=0.0.3" | ||||
| typeguard = ">=2.13.3,<5.0.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "aws-cdk-asset-node-proxy-agent-v6" | ||||
| version = "2.1.0" | ||||
| @ -392,17 +408,18 @@ typeguard = ">=2.13.3,<4.3.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "aws-cdk-lib" | ||||
| version = "2.179.0" | ||||
| version = "2.178.2" | ||||
| description = "Version 2 of the AWS Cloud Development Kit library" | ||||
| optional = false | ||||
| python-versions = "~=3.8" | ||||
| files = [ | ||||
|     {file = "aws_cdk_lib-2.179.0-py3-none-any.whl", hash = "sha256:1d7b88ee69067b8d58dac9eeb6697bbaf5d5c032a3070898389c41e7c4f3e3d7"}, | ||||
|     {file = "aws_cdk_lib-2.179.0.tar.gz", hash = "sha256:b653a55754f4020a4b36e4ae183d213e76e27b18b842cbf9e430e9eccb700550"}, | ||||
|     {file = "aws_cdk_lib-2.178.2-py3-none-any.whl", hash = "sha256:624383e57fe2b32f7d0fc098b78b4cd21d19ae3af3f24b01f32ec4795baaee25"}, | ||||
|     {file = "aws_cdk_lib-2.178.2.tar.gz", hash = "sha256:c00757885b74023350bb34f388f6447155e802ecf827e595bda917098a4925fe"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| "aws-cdk.asset-awscli-v1" = ">=2.2.208,<3.0.0" | ||||
| "aws-cdk.asset-kubectl-v20" = ">=2.1.3,<3.0.0" | ||||
| "aws-cdk.asset-node-proxy-agent-v6" = ">=2.1.0,<3.0.0" | ||||
| "aws-cdk.cloud-assembly-schema" = ">=39.2.0,<40.0.0" | ||||
| constructs = ">=10.0.0,<11.0.0" | ||||
| @ -449,13 +466,13 @@ typing-extensions = ">=4.0.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "bandit" | ||||
| version = "1.8.3" | ||||
| version = "1.8.2" | ||||
| description = "Security oriented static analyser for python code." | ||||
| optional = false | ||||
| python-versions = ">=3.9" | ||||
| files = [ | ||||
|     {file = "bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8"}, | ||||
|     {file = "bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a"}, | ||||
|     {file = "bandit-1.8.2-py3-none-any.whl", hash = "sha256:df6146ad73dd30e8cbda4e29689ddda48364e36ff655dbfc86998401fcf1721f"}, | ||||
|     {file = "bandit-1.8.2.tar.gz", hash = "sha256:e00ad5a6bc676c0954669fe13818024d66b70e42cf5adb971480cf3b671e835f"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @ -1400,13 +1417,13 @@ files = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "django-filter" | ||||
| version = "25.1" | ||||
| version = "24.3" | ||||
| description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." | ||||
| optional = false | ||||
| python-versions = ">=3.9" | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80"}, | ||||
|     {file = "django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153"}, | ||||
|     {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, | ||||
|     {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @ -1503,13 +1520,13 @@ hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "django-storages" | ||||
| version = "1.14.5" | ||||
| version = "1.14.4" | ||||
| description = "Support for many storage backends in Django" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "django_storages-1.14.5-py3-none-any.whl", hash = "sha256:5ce9c69426f24f379821fd688442314e4aa03de87ae43183c4e16915f4c165d4"}, | ||||
|     {file = "django_storages-1.14.5.tar.gz", hash = "sha256:ace80dbee311258453e30cd5cfd91096b834180ccf09bc1f4d2cb6d38d68571a"}, | ||||
|     {file = "django-storages-1.14.4.tar.gz", hash = "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f"}, | ||||
|     {file = "django_storages-1.14.4-py3-none-any.whl", hash = "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @ -1520,7 +1537,7 @@ Django = ">=3.2" | ||||
| azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"] | ||||
| boto3 = ["boto3 (>=1.4.4)"] | ||||
| dropbox = ["dropbox (>=7.2.1)"] | ||||
| google = ["google-cloud-storage (>=1.32)"] | ||||
| google = ["google-cloud-storage (>=1.27)"] | ||||
| libcloud = ["apache-libcloud"] | ||||
| s3 = ["boto3 (>=1.4.4)"] | ||||
| sftp = ["paramiko (>=1.15)"] | ||||
| @ -1867,17 +1884,6 @@ files = [ | ||||
|     {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "geographiclib" | ||||
| version = "2.0" | ||||
| description = "The geodesic routines from GeographicLib" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734"}, | ||||
|     {file = "geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859"}, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "geoip2" | ||||
| version = "5.0.1" | ||||
| @ -1897,29 +1903,6 @@ requests = ">=2.24.0,<3.0.0" | ||||
| [package.extras] | ||||
| test = ["pytest-httpserver (>=1.0.10)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "geopy" | ||||
| version = "2.4.1" | ||||
| description = "Python Geocoding Toolbox" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7"}, | ||||
|     {file = "geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| geographiclib = ">=1.52,<3" | ||||
|  | ||||
| [package.extras] | ||||
| aiohttp = ["aiohttp"] | ||||
| dev = ["coverage", "flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"] | ||||
| dev-docs = ["readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"] | ||||
| dev-lint = ["flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)"] | ||||
| dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (<=4.3.2)"] | ||||
| requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"] | ||||
| timezone = ["pytz"] | ||||
|  | ||||
| [[package]] | ||||
| name = "google-api-core" | ||||
| version = "2.19.1" | ||||
| @ -4628,13 +4611,13 @@ websocket-client = ">=1.8,<2.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "sentry-sdk" | ||||
| version = "2.22.0" | ||||
| version = "2.21.0" | ||||
| description = "Python client for Sentry (https://sentry.io)" | ||||
| optional = false | ||||
| python-versions = ">=3.6" | ||||
| files = [ | ||||
|     {file = "sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66"}, | ||||
|     {file = "sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944"}, | ||||
|     {file = "sentry_sdk-2.21.0-py2.py3-none-any.whl", hash = "sha256:7623cfa9e2c8150948a81ca253b8e2bfe4ce0b96ab12f8cd78e3ac9c490fd92f"}, | ||||
|     {file = "sentry_sdk-2.21.0.tar.gz", hash = "sha256:a6d38e0fb35edda191acf80b188ec713c863aaa5ad8d5798decb8671d02077b6"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @ -4678,7 +4661,6 @@ sanic = ["sanic (>=0.8)"] | ||||
| sqlalchemy = ["sqlalchemy (>=1.2)"] | ||||
| starlette = ["starlette (>=0.19.1)"] | ||||
| starlite = ["starlite (>=1.48)"] | ||||
| statsig = ["statsig (>=0.55.3)"] | ||||
| tornado = ["tornado (>=6)"] | ||||
| unleash = ["UnleashClient (>=6.0.1)"] | ||||
|  | ||||
| @ -5865,4 +5847,4 @@ files = [ | ||||
| [metadata] | ||||
| lock-version = "2.0" | ||||
| python-versions = "~3.12" | ||||
| content-hash = "8a6bfd4833e415a9f4f613ab4f33e60c8332b9f5743583222cdb7190f6286216" | ||||
| content-hash = "a3915ac2ef2bb53f7cd67070912cdaf717c3bf73ed972fa337a9b07fce162451" | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [tool.poetry] | ||||
| name = "authentik" | ||||
| version = "2025.2.0" | ||||
| version = "2024.12.3" | ||||
| description = "" | ||||
| authors = ["authentik Team <hello@goauthentik.io>"] | ||||
|  | ||||
| @ -113,7 +113,6 @@ duo-client = "*" | ||||
| fido2 = "*" | ||||
| flower = "*" | ||||
| geoip2 = "*" | ||||
| geopy = "*" | ||||
| google-api-python-client = "*" | ||||
| gunicorn = "*" | ||||
| gssapi = "*" | ||||
|  | ||||
							
								
								
									
										1238
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										1238
									
								
								schema.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -74,7 +74,7 @@ const interfaces = [ | ||||
|     ["user/UserInterface.ts", "user"], | ||||
|     ["flow/FlowInterface.ts", "flow"], | ||||
|     ["standalone/api-browser/index.ts", "standalone/api-browser"], | ||||
|     ["rac/index.ts", "rac"], | ||||
|     ["enterprise/rac/index.ts", "enterprise/rac"], | ||||
|     ["standalone/loading/index.ts", "standalone/loading"], | ||||
|     ["polyfill/poly.ts", "."], | ||||
| ]; | ||||
|  | ||||
							
								
								
									
										350
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										350
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -23,7 +23,7 @@ | ||||
|                 "@floating-ui/dom": "^1.6.11", | ||||
|                 "@formatjs/intl-listformat": "^7.5.7", | ||||
|                 "@fortawesome/fontawesome-free": "^6.6.0", | ||||
|                 "@goauthentik/api": "^2024.12.3-1739965710", | ||||
|                 "@goauthentik/api": "^2024.12.3-1739449824", | ||||
|                 "@lit-labs/ssr": "^3.2.2", | ||||
|                 "@lit/context": "^1.1.2", | ||||
|                 "@lit/localize": "^0.12.2", | ||||
| @ -42,12 +42,12 @@ | ||||
|                 "construct-style-sheets-polyfill": "^3.1.0", | ||||
|                 "core-js": "^3.38.1", | ||||
|                 "country-flag-icons": "^1.5.13", | ||||
|                 "dompurify": "^3.2.4", | ||||
|                 "dompurify": "^3.1.7", | ||||
|                 "fuse.js": "^7.0.0", | ||||
|                 "guacamole-common-js": "^1.5.0", | ||||
|                 "lit": "^3.2.0", | ||||
|                 "md-front-matter": "^1.0.4", | ||||
|                 "mermaid": "^11.4.1", | ||||
|                 "mermaid": "^11.2.1", | ||||
|                 "rapidoc": "^9.3.7", | ||||
|                 "showdown": "^2.1.0", | ||||
|                 "style-mod": "^4.1.2", | ||||
| @ -1814,9 +1814,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@goauthentik/api": { | ||||
|             "version": "2024.12.3-1739965710", | ||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739965710.tgz", | ||||
|             "integrity": "sha512-16zoQWeJhAFSwttvqLRoXoQA43tMW1ZXDEihW6r8rtWtlxqPh7n36RtcWYraYiLcjmJskI90zdgz6k1kmY5AXw==" | ||||
|             "version": "2024.12.3-1739449824", | ||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739449824.tgz", | ||||
|             "integrity": "sha512-0M2SkvqpdjYgWOtaRLO41gTTyo43WPXlWbcfqCxfCJUoi1c3VGT5mozFCgRM21mY6+a3tKPHh4O28qDuz5gthw==" | ||||
|         }, | ||||
|         "node_modules/@goauthentik/web": { | ||||
|             "resolved": "", | ||||
| @ -5728,259 +5728,6 @@ | ||||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3": { | ||||
|             "version": "7.4.3", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", | ||||
|             "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-array": "*", | ||||
|                 "@types/d3-axis": "*", | ||||
|                 "@types/d3-brush": "*", | ||||
|                 "@types/d3-chord": "*", | ||||
|                 "@types/d3-color": "*", | ||||
|                 "@types/d3-contour": "*", | ||||
|                 "@types/d3-delaunay": "*", | ||||
|                 "@types/d3-dispatch": "*", | ||||
|                 "@types/d3-drag": "*", | ||||
|                 "@types/d3-dsv": "*", | ||||
|                 "@types/d3-ease": "*", | ||||
|                 "@types/d3-fetch": "*", | ||||
|                 "@types/d3-force": "*", | ||||
|                 "@types/d3-format": "*", | ||||
|                 "@types/d3-geo": "*", | ||||
|                 "@types/d3-hierarchy": "*", | ||||
|                 "@types/d3-interpolate": "*", | ||||
|                 "@types/d3-path": "*", | ||||
|                 "@types/d3-polygon": "*", | ||||
|                 "@types/d3-quadtree": "*", | ||||
|                 "@types/d3-random": "*", | ||||
|                 "@types/d3-scale": "*", | ||||
|                 "@types/d3-scale-chromatic": "*", | ||||
|                 "@types/d3-selection": "*", | ||||
|                 "@types/d3-shape": "*", | ||||
|                 "@types/d3-time": "*", | ||||
|                 "@types/d3-time-format": "*", | ||||
|                 "@types/d3-timer": "*", | ||||
|                 "@types/d3-transition": "*", | ||||
|                 "@types/d3-zoom": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-array": { | ||||
|             "version": "3.2.1", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", | ||||
|             "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-axis": { | ||||
|             "version": "3.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", | ||||
|             "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-selection": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-brush": { | ||||
|             "version": "3.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", | ||||
|             "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-selection": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-chord": { | ||||
|             "version": "3.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", | ||||
|             "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-color": { | ||||
|             "version": "3.1.3", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", | ||||
|             "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-contour": { | ||||
|             "version": "3.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", | ||||
|             "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-array": "*", | ||||
|                 "@types/geojson": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-delaunay": { | ||||
|             "version": "6.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", | ||||
|             "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-dispatch": { | ||||
|             "version": "3.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", | ||||
|             "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-drag": { | ||||
|             "version": "3.0.7", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", | ||||
|             "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-selection": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-dsv": { | ||||
|             "version": "3.0.7", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", | ||||
|             "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-ease": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", | ||||
|             "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-fetch": { | ||||
|             "version": "3.0.7", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", | ||||
|             "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-dsv": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-force": { | ||||
|             "version": "3.0.10", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", | ||||
|             "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-format": { | ||||
|             "version": "3.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", | ||||
|             "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-geo": { | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", | ||||
|             "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/geojson": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-hierarchy": { | ||||
|             "version": "3.1.7", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", | ||||
|             "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-interpolate": { | ||||
|             "version": "3.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", | ||||
|             "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-color": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-path": { | ||||
|             "version": "3.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", | ||||
|             "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-polygon": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", | ||||
|             "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-quadtree": { | ||||
|             "version": "3.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", | ||||
|             "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-random": { | ||||
|             "version": "3.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", | ||||
|             "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-scale": { | ||||
|             "version": "4.0.9", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", | ||||
|             "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-time": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-scale-chromatic": { | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", | ||||
|             "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-selection": { | ||||
|             "version": "3.0.11", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", | ||||
|             "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-shape": { | ||||
|             "version": "3.1.7", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", | ||||
|             "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-path": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-time": { | ||||
|             "version": "3.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", | ||||
|             "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-time-format": { | ||||
|             "version": "4.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", | ||||
|             "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-timer": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", | ||||
|             "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/d3-transition": { | ||||
|             "version": "3.0.9", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", | ||||
|             "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-selection": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/d3-zoom": { | ||||
|             "version": "3.0.8", | ||||
|             "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", | ||||
|             "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/d3-interpolate": "*", | ||||
|                 "@types/d3-selection": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/dompurify": { | ||||
|             "version": "3.0.5", | ||||
|             "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", | ||||
| @ -6055,12 +5802,6 @@ | ||||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/geojson": { | ||||
|             "version": "7946.0.16", | ||||
|             "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", | ||||
|             "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/glob": { | ||||
|             "version": "7.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", | ||||
| @ -10241,7 +9982,6 @@ | ||||
|             "version": "7.9.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", | ||||
|             "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-array": "3", | ||||
|                 "d3-axis": "3", | ||||
| @ -10282,7 +10022,6 @@ | ||||
|             "version": "3.2.4", | ||||
|             "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", | ||||
|             "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "internmap": "1 - 2" | ||||
|             }, | ||||
| @ -10294,7 +10033,6 @@ | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", | ||||
|             "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10303,7 +10041,6 @@ | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", | ||||
|             "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-dispatch": "1 - 3", | ||||
|                 "d3-drag": "2 - 3", | ||||
| @ -10319,7 +10056,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", | ||||
|             "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-path": "1 - 3" | ||||
|             }, | ||||
| @ -10331,7 +10067,6 @@ | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", | ||||
|             "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10340,7 +10075,6 @@ | ||||
|             "version": "4.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", | ||||
|             "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-array": "^3.2.0" | ||||
|             }, | ||||
| @ -10352,7 +10086,6 @@ | ||||
|             "version": "6.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", | ||||
|             "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "delaunator": "5" | ||||
|             }, | ||||
| @ -10364,7 +10097,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", | ||||
|             "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10373,7 +10105,6 @@ | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", | ||||
|             "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-dispatch": "1 - 3", | ||||
|                 "d3-selection": "3" | ||||
| @ -10386,7 +10117,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", | ||||
|             "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "commander": "7", | ||||
|                 "iconv-lite": "0.6", | ||||
| @ -10411,7 +10141,6 @@ | ||||
|             "version": "7.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", | ||||
|             "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", | ||||
|             "license": "MIT", | ||||
|             "engines": { | ||||
|                 "node": ">= 10" | ||||
|             } | ||||
| @ -10420,7 +10149,6 @@ | ||||
|             "version": "0.6.3", | ||||
|             "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", | ||||
|             "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "safer-buffer": ">= 2.1.2 < 3.0.0" | ||||
|             }, | ||||
| @ -10432,7 +10160,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", | ||||
|             "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", | ||||
|             "license": "BSD-3-Clause", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10441,7 +10168,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", | ||||
|             "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-dsv": "1 - 3" | ||||
|             }, | ||||
| @ -10453,7 +10179,6 @@ | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", | ||||
|             "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-dispatch": "1 - 3", | ||||
|                 "d3-quadtree": "1 - 3", | ||||
| @ -10467,7 +10192,6 @@ | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", | ||||
|             "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10476,7 +10200,6 @@ | ||||
|             "version": "3.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", | ||||
|             "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-array": "2.5.0 - 3" | ||||
|             }, | ||||
| @ -10488,7 +10211,6 @@ | ||||
|             "version": "3.1.2", | ||||
|             "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", | ||||
|             "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10497,7 +10219,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", | ||||
|             "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-color": "1 - 3" | ||||
|             }, | ||||
| @ -10509,7 +10230,6 @@ | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", | ||||
|             "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10518,7 +10238,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", | ||||
|             "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10527,7 +10246,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", | ||||
|             "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10536,7 +10254,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", | ||||
|             "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10580,7 +10297,6 @@ | ||||
|             "version": "4.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", | ||||
|             "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-array": "2.10.0 - 3", | ||||
|                 "d3-format": "1 - 3", | ||||
| @ -10596,7 +10312,6 @@ | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", | ||||
|             "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-color": "1 - 3", | ||||
|                 "d3-interpolate": "1 - 3" | ||||
| @ -10609,7 +10324,6 @@ | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", | ||||
|             "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10618,7 +10332,6 @@ | ||||
|             "version": "3.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", | ||||
|             "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-path": "^3.1.0" | ||||
|             }, | ||||
| @ -10630,7 +10343,6 @@ | ||||
|             "version": "3.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", | ||||
|             "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-array": "2 - 3" | ||||
|             }, | ||||
| @ -10642,7 +10354,6 @@ | ||||
|             "version": "4.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", | ||||
|             "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-time": "1 - 3" | ||||
|             }, | ||||
| @ -10654,7 +10365,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", | ||||
|             "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -10663,7 +10373,6 @@ | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", | ||||
|             "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-color": "1 - 3", | ||||
|                 "d3-dispatch": "1 - 3", | ||||
| @ -10682,7 +10391,6 @@ | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", | ||||
|             "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "d3-dispatch": "1 - 3", | ||||
|                 "d3-drag": "2 - 3", | ||||
| @ -10695,12 +10403,11 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/dagre-d3-es": { | ||||
|             "version": "7.0.11", | ||||
|             "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", | ||||
|             "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", | ||||
|             "license": "MIT", | ||||
|             "version": "7.0.10", | ||||
|             "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", | ||||
|             "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", | ||||
|             "dependencies": { | ||||
|                 "d3": "^7.9.0", | ||||
|                 "d3": "^7.8.2", | ||||
|                 "lodash-es": "^4.17.21" | ||||
|             } | ||||
|         }, | ||||
| @ -10939,7 +10646,6 @@ | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", | ||||
|             "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "robust-predicates": "^3.0.2" | ||||
|             } | ||||
| @ -11091,13 +10797,10 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/dompurify": { | ||||
|             "version": "3.2.4", | ||||
|             "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", | ||||
|             "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", | ||||
|             "license": "(MPL-2.0 OR Apache-2.0)", | ||||
|             "optionalDependencies": { | ||||
|                 "@types/trusted-types": "^2.0.7" | ||||
|             } | ||||
|             "version": "3.1.7", | ||||
|             "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", | ||||
|             "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", | ||||
|             "license": "(MPL-2.0 OR Apache-2.0)" | ||||
|         }, | ||||
|         "node_modules/domutils": { | ||||
|             "version": "3.1.0", | ||||
| @ -13955,7 +13658,6 @@ | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", | ||||
|             "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", | ||||
|             "license": "ISC", | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             } | ||||
| @ -16080,23 +15782,21 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/mermaid": { | ||||
|             "version": "11.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz", | ||||
|             "integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==", | ||||
|             "license": "MIT", | ||||
|             "version": "11.3.0", | ||||
|             "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.3.0.tgz", | ||||
|             "integrity": "sha512-fFmf2gRXLtlGzug4wpIGN+rQdZ30M8IZEB1D3eZkXNqC7puhqeURBcD/9tbwXsqBO+A6Nzzo3MSSepmnw5xSeg==", | ||||
|             "dependencies": { | ||||
|                 "@braintree/sanitize-url": "^7.0.1", | ||||
|                 "@iconify/utils": "^2.1.32", | ||||
|                 "@mermaid-js/parser": "^0.3.0", | ||||
|                 "@types/d3": "^7.4.3", | ||||
|                 "cytoscape": "^3.29.2", | ||||
|                 "cytoscape-cose-bilkent": "^4.1.0", | ||||
|                 "cytoscape-fcose": "^2.2.0", | ||||
|                 "d3": "^7.9.0", | ||||
|                 "d3-sankey": "^0.12.3", | ||||
|                 "dagre-d3-es": "7.0.11", | ||||
|                 "dagre-d3-es": "7.0.10", | ||||
|                 "dayjs": "^1.11.10", | ||||
|                 "dompurify": "^3.2.1", | ||||
|                 "dompurify": "^3.0.11 <3.1.7", | ||||
|                 "katex": "^0.16.9", | ||||
|                 "khroma": "^2.1.0", | ||||
|                 "lodash-es": "^4.17.21", | ||||
| @ -16107,6 +15807,12 @@ | ||||
|                 "uuid": "^9.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/mermaid/node_modules/dompurify": { | ||||
|             "version": "3.1.6", | ||||
|             "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", | ||||
|             "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", | ||||
|             "license": "(MPL-2.0 OR Apache-2.0)" | ||||
|         }, | ||||
|         "node_modules/methods": { | ||||
|             "version": "1.1.2", | ||||
|             "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", | ||||
| @ -19278,8 +18984,7 @@ | ||||
|         "node_modules/robust-predicates": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", | ||||
|             "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", | ||||
|             "license": "Unlicense" | ||||
|             "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" | ||||
|         }, | ||||
|         "node_modules/rollup": { | ||||
|             "version": "4.24.0", | ||||
| @ -19549,8 +19254,7 @@ | ||||
|         "node_modules/rw": { | ||||
|             "version": "1.3.3", | ||||
|             "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", | ||||
|             "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", | ||||
|             "license": "BSD-3-Clause" | ||||
|             "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" | ||||
|         }, | ||||
|         "node_modules/rxjs": { | ||||
|             "version": "7.8.1", | ||||
|  | ||||
| @ -11,7 +11,7 @@ | ||||
|         "@floating-ui/dom": "^1.6.11", | ||||
|         "@formatjs/intl-listformat": "^7.5.7", | ||||
|         "@fortawesome/fontawesome-free": "^6.6.0", | ||||
|         "@goauthentik/api": "^2024.12.3-1739965710", | ||||
|         "@goauthentik/api": "^2024.12.3-1739449824", | ||||
|         "@lit-labs/ssr": "^3.2.2", | ||||
|         "@lit/context": "^1.1.2", | ||||
|         "@lit/localize": "^0.12.2", | ||||
| @ -30,12 +30,12 @@ | ||||
|         "construct-style-sheets-polyfill": "^3.1.0", | ||||
|         "core-js": "^3.38.1", | ||||
|         "country-flag-icons": "^1.5.13", | ||||
|         "dompurify": "^3.2.4", | ||||
|         "dompurify": "^3.1.7", | ||||
|         "fuse.js": "^7.0.0", | ||||
|         "guacamole-common-js": "^1.5.0", | ||||
|         "lit": "^3.2.0", | ||||
|         "md-front-matter": "^1.0.4", | ||||
|         "mermaid": "^11.4.1", | ||||
|         "mermaid": "^11.2.1", | ||||
|         "rapidoc": "^9.3.7", | ||||
|         "showdown": "^2.1.0", | ||||
|         "style-mod": "^4.1.2", | ||||
|  | ||||
| @ -6,7 +6,7 @@ const config: KnipConfig = { | ||||
|         "./src/user/UserInterface.ts", | ||||
|         "./src/flow/FlowInterface.ts", | ||||
|         "./src/standalone/api-browser/index.ts", | ||||
|         "./src/rac/index.ts", | ||||
|         "./src/enterprise/rac/index.ts", | ||||
|         "./src/standalone/loading/index.ts", | ||||
|         "./src/polyfill/poly.ts", | ||||
|     ], | ||||
|  | ||||
| @ -7,7 +7,6 @@ import "@goauthentik/components/ak-radio-input"; | ||||
| import "@goauthentik/components/ak-switch-input"; | ||||
| import "@goauthentik/components/ak-text-input"; | ||||
| import "@goauthentik/components/ak-textarea-input"; | ||||
| import "@goauthentik/elements/Alert.js"; | ||||
| import { | ||||
|     CapabilitiesEnum, | ||||
|     WithCapabilitiesConfig, | ||||
| @ -22,7 +21,7 @@ import "@goauthentik/elements/forms/SearchSelect"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { TemplateResult, html, nothing } from "lit"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| @ -121,12 +120,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         const alertMsg = msg( | ||||
|             "Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.", | ||||
|         ); | ||||
|  | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`} | ||||
|             <ak-text-input | ||||
|                 name="name" | ||||
|                 value=${ifDefined(this.instance?.name)} | ||||
|  | ||||
| @ -50,7 +50,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>) | ||||
|     } | ||||
|     pageDescription(): string { | ||||
|         return msg( | ||||
|             str`External applications that use ${this.brand?.brandingTitle ?? "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`, | ||||
|             str`External applications that use ${this.brand.brandingTitle || "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`, | ||||
|         ); | ||||
|     } | ||||
|     pageIcon(): string { | ||||
| @ -85,6 +85,10 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>) | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderSectionBefore(): TemplateResult { | ||||
|         return html`<ak-application-wizard-hint></ak-application-wizard-hint>`; | ||||
|     } | ||||
|  | ||||
|     renderSidebarAfter(): TemplateResult { | ||||
|         return html`<div class="pf-c-sidebar__panel pf-m-width-25"> | ||||
|             <div class="pf-c-card"> | ||||
| @ -156,21 +160,12 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>) | ||||
|     } | ||||
|  | ||||
|     renderObjectCreate(): TemplateResult { | ||||
|         return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}> | ||||
|                 <button | ||||
|                     slot="trigger" | ||||
|                     class="pf-c-button pf-m-primary" | ||||
|                     data-ouia-component-id="start-application-wizard" | ||||
|                 > | ||||
|                     ${msg("Create with Provider")} | ||||
|                 </button> | ||||
|             </ak-application-wizard> | ||||
|             <ak-forms-modal .open=${getURLParam("createForm", false)}> | ||||
|                 <span slot="submit"> ${msg("Create")} </span> | ||||
|                 <span slot="header"> ${msg("Create Application")} </span> | ||||
|                 <ak-application-form slot="form"> </ak-application-form> | ||||
|                 <button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button> | ||||
|             </ak-forms-modal>`; | ||||
|         return html`<ak-forms-modal .open=${getURLParam("createForm", false)}> | ||||
|             <span slot="submit"> ${msg("Create")} </span> | ||||
|             <span slot="header"> ${msg("Create Application")} </span> | ||||
|             <ak-application-form slot="form"> </ak-application-form> | ||||
|             <button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button> | ||||
|         </ak-forms-modal>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	