Compare commits
	
		
			12 Commits
		
	
	
		
			website/in
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2739376a2a | |||
| 152121175b | |||
| 1d57a258f3 | |||
| f15cac39c8 | |||
| ce77d82b24 | |||
| c3fe57197d | |||
| 267938d435 | |||
| 6a7c2e0662 | |||
| 5336afb1b4 | |||
| 9bb44055a3 | |||
| 143663d293 | |||
| bd54d034e1 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2023.5.3 | current_version = 2023.5.4 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from os import environ | from os import environ | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| __version__ = "2023.5.3" | __version__ = "2023.5.4" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| """authentik administration overview""" | """authentik administration overview""" | ||||||
| import os |  | ||||||
| import platform | import platform | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from sys import version as python_version | from sys import version as python_version | ||||||
| @ -34,7 +33,6 @@ class RuntimeDict(TypedDict): | |||||||
| class SystemSerializer(PassiveSerializer): | class SystemSerializer(PassiveSerializer): | ||||||
|     """Get system information.""" |     """Get system information.""" | ||||||
|  |  | ||||||
|     env = SerializerMethodField() |  | ||||||
|     http_headers = SerializerMethodField() |     http_headers = SerializerMethodField() | ||||||
|     http_host = SerializerMethodField() |     http_host = SerializerMethodField() | ||||||
|     http_is_secure = SerializerMethodField() |     http_is_secure = SerializerMethodField() | ||||||
| @ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer): | |||||||
|     server_time = SerializerMethodField() |     server_time = SerializerMethodField() | ||||||
|     embedded_outpost_host = SerializerMethodField() |     embedded_outpost_host = SerializerMethodField() | ||||||
|  |  | ||||||
|     def get_env(self, request: Request) -> dict[str, str]: |  | ||||||
|         """Get Environment""" |  | ||||||
|         return os.environ.copy() |  | ||||||
|  |  | ||||||
|     def get_http_headers(self, request: Request) -> dict[str, str]: |     def get_http_headers(self, request: Request) -> dict[str, str]: | ||||||
|         """Get HTTP Request headers""" |         """Get HTTP Request headers""" | ||||||
|         headers = {} |         headers = {} | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """API Authentication""" | """API Authentication""" | ||||||
|  | from hmac import compare_digest | ||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]: | |||||||
|     and return the service account for the managed outpost""" |     and return the service account for the managed outpost""" | ||||||
|     from authentik.outposts.apps import MANAGED_OUTPOST |     from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
|  |  | ||||||
|     if value != settings.SECRET_KEY: |     if not compare_digest(value, settings.SECRET_KEY): | ||||||
|         return None |         return None | ||||||
|     outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) |     outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) | ||||||
|     if not outposts: |     if not outposts: | ||||||
|  | |||||||
| @ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): | |||||||
|     def retrieve_file(self) -> str: |     def retrieve_file(self) -> str: | ||||||
|         """Get blueprint from path""" |         """Get blueprint from path""" | ||||||
|         try: |         try: | ||||||
|             full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path)) |             base = Path(CONFIG.y("blueprints_dir")) | ||||||
|  |             full_path = base.joinpath(Path(self.path)).resolve() | ||||||
|  |             if not str(full_path).startswith(str(base.resolve())): | ||||||
|  |                 raise BlueprintRetrievalFailed("Invalid blueprint path") | ||||||
|             with full_path.open("r", encoding="utf-8") as _file: |             with full_path.open("r", encoding="utf-8") as _file: | ||||||
|                 return _file.read() |                 return _file.read() | ||||||
|         except (IOError, OSError) as exc: |         except (IOError, OSError) as exc: | ||||||
|  | |||||||
| @ -1,34 +1,15 @@ | |||||||
| """authentik managed models tests""" | """authentik managed models tests""" | ||||||
| from typing import Callable, Type |  | ||||||
|  |  | ||||||
| from django.apps import apps |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.v1.importer import is_model_allowed | from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed | ||||||
| from authentik.lib.models import SerializerModel | from authentik.lib.generators import generate_id | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestModels(TestCase): | class TestModels(TestCase): | ||||||
|     """Test Models""" |     """Test Models""" | ||||||
|  |  | ||||||
|  |     def test_retrieve_file(self): | ||||||
| def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable: |         """Test retrieve_file""" | ||||||
|     """Test serializer""" |         instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts") | ||||||
|  |         with self.assertRaises(BlueprintRetrievalFailed): | ||||||
|     def tester(self: TestModels): |             instance.retrieve() | ||||||
|         if test_model._meta.abstract:  # pragma: no cover |  | ||||||
|             return |  | ||||||
|         model_class = test_model() |  | ||||||
|         self.assertTrue(isinstance(model_class, SerializerModel)) |  | ||||||
|         self.assertIsNotNone(model_class.serializer) |  | ||||||
|  |  | ||||||
|     return tester |  | ||||||
|  |  | ||||||
|  |  | ||||||
| for app in apps.get_app_configs(): |  | ||||||
|     if not app.label.startswith("authentik"): |  | ||||||
|         continue |  | ||||||
|     for model in app.get_models(): |  | ||||||
|         if not is_model_allowed(model): |  | ||||||
|             continue |  | ||||||
|         setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) |  | ||||||
|  | |||||||
							
								
								
									
										34
									
								
								authentik/blueprints/tests/test_serializer_models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								authentik/blueprints/tests/test_serializer_models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | """authentik managed models tests""" | ||||||
|  | from typing import Callable, Type | ||||||
|  |  | ||||||
|  | from django.apps import apps | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.blueprints.v1.importer import is_model_allowed | ||||||
|  | from authentik.lib.models import SerializerModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestModels(TestCase): | ||||||
|  |     """Test Models""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable: | ||||||
|  |     """Test serializer""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         if test_model._meta.abstract:  # pragma: no cover | ||||||
|  |             return | ||||||
|  |         model_class = test_model() | ||||||
|  |         self.assertTrue(isinstance(model_class, SerializerModel)) | ||||||
|  |         self.assertIsNotNone(model_class.serializer) | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for app in apps.get_app_configs(): | ||||||
|  |     if not app.label.startswith("authentik"): | ||||||
|  |         continue | ||||||
|  |     for model in app.get_models(): | ||||||
|  |         if not is_model_allowed(model): | ||||||
|  |             continue | ||||||
|  |         setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) | ||||||
| @ -67,11 +67,12 @@ from authentik.core.models import ( | |||||||
|     TokenIntents, |     TokenIntents, | ||||||
|     User, |     User, | ||||||
| ) | ) | ||||||
| from authentik.events.models import EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.exceptions import FlowNonApplicableException | from authentik.flows.exceptions import FlowNonApplicableException | ||||||
| from authentik.flows.models import FlowToken | from authentik.flows.models import FlowToken | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | from authentik.flows.views.executor import QS_KEY_TOKEN | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| @ -543,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|         send_mails(email_stage, message) |         send_mails(email_stage, message) | ||||||
|         return Response(status=204) |         return Response(status=204) | ||||||
|  |  | ||||||
|  |     @permission_required("authentik_core.impersonate") | ||||||
|  |     @extend_schema( | ||||||
|  |         request=OpenApiTypes.NONE, | ||||||
|  |         responses={ | ||||||
|  |             "204": OpenApiResponse(description="Successfully started impersonation"), | ||||||
|  |             "401": OpenApiResponse(description="Access denied"), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     @action(detail=True, methods=["POST"]) | ||||||
|  |     def impersonate(self, request: Request, pk: int) -> Response: | ||||||
|  |         """Impersonate a user""" | ||||||
|  |         if not CONFIG.y_bool("impersonation"): | ||||||
|  |             LOGGER.debug("User attempted to impersonate", user=request.user) | ||||||
|  |             return Response(status=401) | ||||||
|  |         if not request.user.has_perm("impersonate"): | ||||||
|  |             LOGGER.debug("User attempted to impersonate without permissions", user=request.user) | ||||||
|  |             return Response(status=401) | ||||||
|  |  | ||||||
|  |         user_to_be = self.get_object() | ||||||
|  |  | ||||||
|  |         request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user | ||||||
|  |         request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be | ||||||
|  |  | ||||||
|  |         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) | ||||||
|  |  | ||||||
|  |         return Response(status=201) | ||||||
|  |  | ||||||
|  |     @extend_schema( | ||||||
|  |         request=OpenApiTypes.NONE, | ||||||
|  |         responses={ | ||||||
|  |             "204": OpenApiResponse(description="Successfully started impersonation"), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     @action(detail=False, methods=["GET"]) | ||||||
|  |     def impersonate_end(self, request: Request) -> Response: | ||||||
|  |         """End Impersonation a user""" | ||||||
|  |         if ( | ||||||
|  |             SESSION_KEY_IMPERSONATE_USER not in request.session | ||||||
|  |             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session | ||||||
|  |         ): | ||||||
|  |             LOGGER.debug("Can't end impersonation", user=request.user) | ||||||
|  |             return Response(status=204) | ||||||
|  |  | ||||||
|  |         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] | ||||||
|  |  | ||||||
|  |         del request.session[SESSION_KEY_IMPERSONATE_USER] | ||||||
|  |         del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] | ||||||
|  |  | ||||||
|  |         Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user) | ||||||
|  |  | ||||||
|  |         return Response(status=204) | ||||||
|  |  | ||||||
|     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: |     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: | ||||||
|         """Custom filter_queryset method which ignores guardian, but still supports sorting""" |         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||||
|         for backend in list(self.filter_backends): |         for backend in list(self.filter_backends): | ||||||
|  | |||||||
| @ -1,14 +1,14 @@ | |||||||
| """impersonation tests""" | """impersonation tests""" | ||||||
| from json import loads | from json import loads | ||||||
|  |  | ||||||
| from django.test.testcases import TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.core.tests.utils import create_test_admin_user | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestImpersonation(TestCase): | class TestImpersonation(APITestCase): | ||||||
|     """impersonation tests""" |     """impersonation tests""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
| @ -23,10 +23,10 @@ class TestImpersonation(TestCase): | |||||||
|         self.other_user.save() |         self.other_user.save() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|         self.client.get( |         self.client.post( | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_core:impersonate-init", |                 "authentik_api:user-impersonate", | ||||||
|                 kwargs={"user_id": self.other_user.pk}, |                 kwargs={"pk": self.other_user.pk}, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @ -35,7 +35,7 @@ class TestImpersonation(TestCase): | |||||||
|         self.assertEqual(response_body["user"]["username"], self.other_user.username) |         self.assertEqual(response_body["user"]["username"], self.other_user.username) | ||||||
|         self.assertEqual(response_body["original"]["username"], self.user.username) |         self.assertEqual(response_body["original"]["username"], self.user.username) | ||||||
|  |  | ||||||
|         self.client.get(reverse("authentik_core:impersonate-end")) |         self.client.get(reverse("authentik_api:user-impersonate-end")) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_api:user-me")) |         response = self.client.get(reverse("authentik_api:user-me")) | ||||||
|         response_body = loads(response.content.decode()) |         response_body = loads(response.content.decode()) | ||||||
| @ -46,9 +46,7 @@ class TestImpersonation(TestCase): | |||||||
|         """test impersonation without permissions""" |         """test impersonation without permissions""" | ||||||
|         self.client.force_login(self.other_user) |         self.client.force_login(self.other_user) | ||||||
|  |  | ||||||
|         self.client.get( |         self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})) | ||||||
|             reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_api:user-me")) |         response = self.client.get(reverse("authentik_api:user-me")) | ||||||
|         response_body = loads(response.content.decode()) |         response_body = loads(response.content.decode()) | ||||||
| @ -58,5 +56,5 @@ class TestImpersonation(TestCase): | |||||||
|         """test un-impersonation without impersonating first""" |         """test un-impersonation without impersonating first""" | ||||||
|         self.client.force_login(self.other_user) |         self.client.force_login(self.other_user) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_core:impersonate-end")) |         response = self.client.get(reverse("authentik_api:user-impersonate-end")) | ||||||
|         self.assertRedirects(response, reverse("authentik_core:if-user")) |         self.assertEqual(response.status_code, 204) | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ from authentik.core.api.providers import ProviderViewSet | |||||||
| from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet | from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet | ||||||
| from authentik.core.api.tokens import TokenViewSet | from authentik.core.api.tokens import TokenViewSet | ||||||
| from authentik.core.api.users import UserViewSet | from authentik.core.api.users import UserViewSet | ||||||
| from authentik.core.views import apps, impersonate | from authentik.core.views import apps | ||||||
| from authentik.core.views.debug import AccessDeniedView | from authentik.core.views.debug import AccessDeniedView | ||||||
| from authentik.core.views.interface import FlowInterfaceView, InterfaceView | from authentik.core.views.interface import FlowInterfaceView, InterfaceView | ||||||
| from authentik.core.views.session import EndSessionView | from authentik.core.views.session import EndSessionView | ||||||
| @ -38,17 +38,6 @@ urlpatterns = [ | |||||||
|         apps.RedirectToAppLaunch.as_view(), |         apps.RedirectToAppLaunch.as_view(), | ||||||
|         name="application-launch", |         name="application-launch", | ||||||
|     ), |     ), | ||||||
|     # Impersonation |  | ||||||
|     path( |  | ||||||
|         "-/impersonation/<int:user_id>/", |  | ||||||
|         impersonate.ImpersonateInitView.as_view(), |  | ||||||
|         name="impersonate-init", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "-/impersonation/end/", |  | ||||||
|         impersonate.ImpersonateEndView.as_view(), |  | ||||||
|         name="impersonate-end", |  | ||||||
|     ), |  | ||||||
|     # Interfaces |     # Interfaces | ||||||
|     path( |     path( | ||||||
|         "if/admin/", |         "if/admin/", | ||||||
|  | |||||||
| @ -1,60 +0,0 @@ | |||||||
| """authentik impersonation views""" |  | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse |  | ||||||
| from django.shortcuts import get_object_or_404, redirect |  | ||||||
| from django.views import View |  | ||||||
| from structlog.stdlib import get_logger |  | ||||||
|  |  | ||||||
| from authentik.core.middleware import ( |  | ||||||
|     SESSION_KEY_IMPERSONATE_ORIGINAL_USER, |  | ||||||
|     SESSION_KEY_IMPERSONATE_USER, |  | ||||||
| ) |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.events.models import Event, EventAction |  | ||||||
| from authentik.lib.config import CONFIG |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ImpersonateInitView(View): |  | ||||||
|     """Initiate Impersonation""" |  | ||||||
|  |  | ||||||
|     def get(self, request: HttpRequest, user_id: int) -> HttpResponse: |  | ||||||
|         """Impersonation handler, checks permissions""" |  | ||||||
|         if not CONFIG.y_bool("impersonation"): |  | ||||||
|             LOGGER.debug("User attempted to impersonate", user=request.user) |  | ||||||
|             return HttpResponse("Unauthorized", status=401) |  | ||||||
|         if not request.user.has_perm("impersonate"): |  | ||||||
|             LOGGER.debug("User attempted to impersonate without permissions", user=request.user) |  | ||||||
|             return HttpResponse("Unauthorized", status=401) |  | ||||||
|  |  | ||||||
|         user_to_be = get_object_or_404(User, pk=user_id) |  | ||||||
|  |  | ||||||
|         request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user |  | ||||||
|         request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be |  | ||||||
|  |  | ||||||
|         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) |  | ||||||
|  |  | ||||||
|         return redirect("authentik_core:if-user") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ImpersonateEndView(View): |  | ||||||
|     """End User impersonation""" |  | ||||||
|  |  | ||||||
|     def get(self, request: HttpRequest) -> HttpResponse: |  | ||||||
|         """End Impersonation handler""" |  | ||||||
|         if ( |  | ||||||
|             SESSION_KEY_IMPERSONATE_USER not in request.session |  | ||||||
|             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session |  | ||||||
|         ): |  | ||||||
|             LOGGER.debug("Can't end impersonation", user=request.user) |  | ||||||
|             return redirect("authentik_core:if-user") |  | ||||||
|  |  | ||||||
|         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] |  | ||||||
|  |  | ||||||
|         del request.session[SESSION_KEY_IMPERSONATE_USER] |  | ||||||
|         del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] |  | ||||||
|  |  | ||||||
|         Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user) |  | ||||||
|  |  | ||||||
|         return redirect("authentik_core:root-redirect") |  | ||||||
| @ -23,7 +23,8 @@ class DiagramElement: | |||||||
|     style: list[str] = field(default_factory=lambda: ["[", "]"]) |     style: list[str] = field(default_factory=lambda: ["[", "]"]) | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}' |         description = self.description.replace('"', "#quot;") | ||||||
|  |         element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}' | ||||||
|         if self.action is not None: |         if self.action is not None: | ||||||
|             if self.action != "": |             if self.action != "": | ||||||
|                 element = f"--{self.action}--> {element}" |                 element = f"--{self.action}--> {element}" | ||||||
|  | |||||||
| @ -204,12 +204,12 @@ class ChallengeStageView(StageView): | |||||||
|         for field, errors in response.errors.items(): |         for field, errors in response.errors.items(): | ||||||
|             for error in errors: |             for error in errors: | ||||||
|                 full_errors.setdefault(field, []) |                 full_errors.setdefault(field, []) | ||||||
|                 full_errors[field].append( |                 field_error = { | ||||||
|                     { |                     "string": str(error), | ||||||
|                         "string": str(error), |                 } | ||||||
|                         "code": error.code, |                 if hasattr(error, "code"): | ||||||
|                     } |                     field_error["code"] = error.code | ||||||
|                 ) |                 full_errors[field].append(field_error) | ||||||
|         challenge_response.initial_data["response_errors"] = full_errors |         challenge_response.initial_data["response_errors"] = full_errors | ||||||
|         if not challenge_response.is_valid(): |         if not challenge_response.is_valid(): | ||||||
|             self.logger.error( |             self.logger.error( | ||||||
|  | |||||||
| @ -132,9 +132,9 @@ class TestPolicyProcess(TestCase): | |||||||
|         ) |         ) | ||||||
|         binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test")) |         binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test")) | ||||||
|  |  | ||||||
|         http_request = self.factory.get(reverse("authentik_core:impersonate-end")) |         http_request = self.factory.get(reverse("authentik_api:user-impersonate-end")) | ||||||
|         http_request.user = self.user |         http_request.user = self.user | ||||||
|         http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end")) |         http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end")) | ||||||
|  |  | ||||||
|         request = PolicyRequest(self.user) |         request = PolicyRequest(self.user) | ||||||
|         request.set_http_request(http_request) |         request.set_http_request(http_request) | ||||||
|  | |||||||
| @ -66,8 +66,8 @@ def ldap_sync_password(sender, user: User, password: str, **_): | |||||||
|     if not sources.exists(): |     if not sources.exists(): | ||||||
|         return |         return | ||||||
|     source = sources.first() |     source = sources.first() | ||||||
|     changer = LDAPPasswordChanger(source) |  | ||||||
|     try: |     try: | ||||||
|  |         changer = LDAPPasswordChanger(source) | ||||||
|         changer.change_password(user, password) |         changer.change_password(user, password) | ||||||
|     except LDAPOperationResult as exc: |     except LDAPOperationResult as exc: | ||||||
|         LOGGER.warning("failed to set LDAP password", exc=exc) |         LOGGER.warning("failed to set LDAP password", exc=exc) | ||||||
|  | |||||||
| @ -133,6 +133,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) - | |||||||
|     device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() |     device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() | ||||||
|     if not device: |     if not device: | ||||||
|         raise ValidationError("Invalid device") |         raise ValidationError("Invalid device") | ||||||
|  |     # We can only check the device's user if the user we're given isn't anonymous | ||||||
|  |     # as this validation is also used for password-less login where webauthn is the very first | ||||||
|  |     # step done by a user. Only if this validation happens at a later stage we can check | ||||||
|  |     # that the device belongs to the user | ||||||
|  |     if not user.is_anonymous and device.user != user: | ||||||
|  |         raise ValidationError("Invalid device") | ||||||
|  |  | ||||||
|     stage: AuthenticatorValidateStage = stage_view.executor.current_stage |     stage: AuthenticatorValidateStage = stage_view.executor.current_stage | ||||||
|  |  | ||||||
|  | |||||||
| @ -36,9 +36,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME | |||||||
|  |  | ||||||
| COOKIE_NAME_MFA = "authentik_mfa" | COOKIE_NAME_MFA = "authentik_mfa" | ||||||
|  |  | ||||||
| SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages" | PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages" | ||||||
| SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage" | PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage" | ||||||
| SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges" | PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges" | ||||||
|  |  | ||||||
|  |  | ||||||
| class SelectableStageSerializer(PassiveSerializer): | class SelectableStageSerializer(PassiveSerializer): | ||||||
| @ -72,8 +72,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | |||||||
|     component = CharField(default="ak-stage-authenticator-validate") |     component = CharField(default="ak-stage-authenticator-validate") | ||||||
|  |  | ||||||
|     def _challenge_allowed(self, classes: list): |     def _challenge_allowed(self, classes: list): | ||||||
|         device_challenges: list[dict] = self.stage.request.session.get( |         device_challenges: list[dict] = self.stage.executor.plan.context.get( | ||||||
|             SESSION_KEY_DEVICE_CHALLENGES, [] |             PLAN_CONTEXT_DEVICE_CHALLENGES, [] | ||||||
|         ) |         ) | ||||||
|         if not any(x["device_class"] in classes for x in device_challenges): |         if not any(x["device_class"] in classes for x in device_challenges): | ||||||
|             raise ValidationError("No compatible device class allowed") |             raise ValidationError("No compatible device class allowed") | ||||||
| @ -103,7 +103,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | |||||||
|         """Check which challenge the user has selected. Actual logic only used for SMS stage.""" |         """Check which challenge the user has selected. Actual logic only used for SMS stage.""" | ||||||
|         # First check if the challenge is valid |         # First check if the challenge is valid | ||||||
|         allowed = False |         allowed = False | ||||||
|         for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []): |         for device_challenge in self.stage.executor.plan.context.get( | ||||||
|  |             PLAN_CONTEXT_DEVICE_CHALLENGES, [] | ||||||
|  |         ): | ||||||
|             if device_challenge.get("device_class", "") == challenge.get( |             if device_challenge.get("device_class", "") == challenge.get( | ||||||
|                 "device_class", "" |                 "device_class", "" | ||||||
|             ) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""): |             ) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""): | ||||||
| @ -121,11 +123,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | |||||||
|  |  | ||||||
|     def validate_selected_stage(self, stage_pk: str) -> str: |     def validate_selected_stage(self, stage_pk: str) -> str: | ||||||
|         """Check that the selected stage is valid""" |         """Check that the selected stage is valid""" | ||||||
|         stages = self.stage.request.session.get(SESSION_KEY_STAGES, []) |         stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, []) | ||||||
|         if not any(str(stage.pk) == stage_pk for stage in stages): |         if not any(str(stage.pk) == stage_pk for stage in stages): | ||||||
|             raise ValidationError("Selected stage is invalid") |             raise ValidationError("Selected stage is invalid") | ||||||
|         self.stage.logger.debug("Setting selected stage to ", stage=stage_pk) |         self.stage.logger.debug("Setting selected stage to ", stage=stage_pk) | ||||||
|         self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk |         self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk | ||||||
|         return stage_pk |         return stage_pk | ||||||
|  |  | ||||||
|     def validate(self, attrs: dict): |     def validate(self, attrs: dict): | ||||||
| @ -230,7 +232,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|             else: |             else: | ||||||
|                 self.logger.debug("No pending user, continuing") |                 self.logger.debug("No pending user, continuing") | ||||||
|                 return self.executor.stage_ok() |                 return self.executor.stage_ok() | ||||||
|         self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges |         self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges | ||||||
|  |  | ||||||
|         # No allowed devices |         # No allowed devices | ||||||
|         if len(challenges) < 1: |         if len(challenges) < 1: | ||||||
| @ -263,23 +265,23 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|         if stage.configuration_stages.count() == 1: |         if stage.configuration_stages.count() == 1: | ||||||
|             next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk) |             next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk) | ||||||
|             self.logger.debug("Single stage configured, auto-selecting", stage=next_stage) |             self.logger.debug("Single stage configured, auto-selecting", stage=next_stage) | ||||||
|             self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage |             self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage | ||||||
|             # Because that normal execution only happens on post, we directly inject it here and |             # Because that normal execution only happens on post, we directly inject it here and | ||||||
|             # return it |             # return it | ||||||
|             self.executor.plan.insert_stage(next_stage) |             self.executor.plan.insert_stage(next_stage) | ||||||
|             return self.executor.stage_ok() |             return self.executor.stage_ok() | ||||||
|         stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses() |         stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses() | ||||||
|         self.request.session[SESSION_KEY_STAGES] = stages |         self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages | ||||||
|         return super().get(self.request, *args, **kwargs) |         return super().get(self.request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: |     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|         res = super().post(request, *args, **kwargs) |         res = super().post(request, *args, **kwargs) | ||||||
|         if ( |         if ( | ||||||
|             SESSION_KEY_SELECTED_STAGE in self.request.session |             PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context | ||||||
|             and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE |             and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE | ||||||
|         ): |         ): | ||||||
|             self.logger.debug("Got selected stage in session, running that") |             self.logger.debug("Got selected stage in context, running that") | ||||||
|             stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE) |             stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE) | ||||||
|             # Because the foreign key to stage.configuration_stage points to |             # Because the foreign key to stage.configuration_stage points to | ||||||
|             # a base stage class, we need to do another lookup |             # a base stage class, we need to do another lookup | ||||||
|             stage = Stage.objects.get_subclass(pk=stage_pk) |             stage = Stage.objects.get_subclass(pk=stage_pk) | ||||||
| @ -290,8 +292,8 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|         return res |         return res | ||||||
|  |  | ||||||
|     def get_challenge(self) -> AuthenticatorValidationChallenge: |     def get_challenge(self) -> AuthenticatorValidationChallenge: | ||||||
|         challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []) |         challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, []) | ||||||
|         stages = self.request.session.get(SESSION_KEY_STAGES, []) |         stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, []) | ||||||
|         stage_challenges = [] |         stage_challenges = [] | ||||||
|         for stage in stages: |         for stage in stages: | ||||||
|             serializer = SelectableStageSerializer( |             serializer = SelectableStageSerializer( | ||||||
| @ -306,6 +308,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|             stage_challenges.append(serializer.data) |             stage_challenges.append(serializer.data) | ||||||
|         return AuthenticatorValidationChallenge( |         return AuthenticatorValidationChallenge( | ||||||
|             data={ |             data={ | ||||||
|  |                 "component": "ak-stage-authenticator-validate", | ||||||
|                 "type": ChallengeTypes.NATIVE.value, |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|                 "device_challenges": challenges, |                 "device_challenges": challenges, | ||||||
|                 "configuration_stages": stage_challenges, |                 "configuration_stages": stage_challenges, | ||||||
| @ -385,8 +388,3 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|                 "device": webauthn_device, |                 "device": webauthn_device, | ||||||
|             } |             } | ||||||
|         return self.set_valid_mfa_cookie(response.device) |         return self.set_valid_mfa_cookie(response.device) | ||||||
|  |  | ||||||
|     def cleanup(self): |  | ||||||
|         self.request.session.pop(SESSION_KEY_STAGES, None) |  | ||||||
|         self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None) |  | ||||||
|         self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None) |  | ||||||
|  | |||||||
| @ -1,26 +1,19 @@ | |||||||
| """Test validator stage""" | """Test validator stage""" | ||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from django.contrib.sessions.middleware import SessionMiddleware |  | ||||||
| from django.test.client import RequestFactory | from django.test.client import RequestFactory | ||||||
| from django.urls.base import reverse | from django.urls.base import reverse | ||||||
| from rest_framework.exceptions import ValidationError |  | ||||||
|  |  | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction | from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.stage import StageView |  | ||||||
| from authentik.flows.tests import FlowTestCase | from authentik.flows.tests import FlowTestCase | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| from authentik.lib.tests.utils import dummy_get_response |  | ||||||
| from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | ||||||
| from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer | from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer | ||||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||||
| from authentik.stages.authenticator_validate.stage import ( | from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES | ||||||
|     SESSION_KEY_DEVICE_CHALLENGES, |  | ||||||
|     AuthenticatorValidationChallengeResponse, |  | ||||||
| ) |  | ||||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | from authentik.stages.identification.models import IdentificationStage, UserFields | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase): | |||||||
|  |  | ||||||
|     def test_validate_selected_challenge(self): |     def test_validate_selected_challenge(self): | ||||||
|         """Test validate_selected_challenge""" |         """Test validate_selected_challenge""" | ||||||
|         # Prepare request with session |         flow = create_test_flow() | ||||||
|         request = self.request_factory.get("/") |         stage = AuthenticatorValidateStage.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             not_configured_action=NotConfiguredAction.CONFIGURE, | ||||||
|  |             device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP], | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         middleware = SessionMiddleware(dummy_get_response) |         session = self.client.session | ||||||
|         middleware.process_request(request) |         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||||
|         request.session[SESSION_KEY_DEVICE_CHALLENGES] = [ |         plan.append_stage(stage) | ||||||
|  |         plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ | ||||||
|             { |             { | ||||||
|                 "device_class": "static", |                 "device_class": "static", | ||||||
|                 "device_uid": "1", |                 "device_uid": "1", | ||||||
| @ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase): | |||||||
|                 "device_uid": "2", |                 "device_uid": "2", | ||||||
|             }, |             }, | ||||||
|         ] |         ] | ||||||
|         request.session.save() |         session[SESSION_KEY_PLAN] = plan | ||||||
|  |         session.save() | ||||||
|  |  | ||||||
|         res = AuthenticatorValidationChallengeResponse() |         response = self.client.post( | ||||||
|         res.stage = StageView(FlowExecutorView()) |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|         res.stage.request = request |             data={ | ||||||
|         with self.assertRaises(ValidationError): |                 "selected_challenge": { | ||||||
|             res.validate_selected_challenge( |  | ||||||
|                 { |  | ||||||
|                     "device_class": "baz", |                     "device_class": "baz", | ||||||
|                     "device_uid": "quox", |                     "device_uid": "quox", | ||||||
|  |                     "challenge": {}, | ||||||
|                 } |                 } | ||||||
|             ) |             }, | ||||||
|         res.validate_selected_challenge( |         ) | ||||||
|             { |         self.assertStageResponse( | ||||||
|                 "device_class": "static", |             response, | ||||||
|                 "device_uid": "1", |             flow, | ||||||
|             } |             response_errors={ | ||||||
|  |                 "selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}] | ||||||
|  |             }, | ||||||
|  |             component="ak-stage-authenticator-validate", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |             data={ | ||||||
|  |                 "selected_challenge": { | ||||||
|  |                     "device_class": "static", | ||||||
|  |                     "device_uid": "1", | ||||||
|  |                     "challenge": {}, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.assertStageResponse( | ||||||
|  |             response, | ||||||
|  |             flow, | ||||||
|  |             response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]}, | ||||||
|  |             component="ak-stage-authenticator-validate", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @patch( |     @patch( | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import ( | |||||||
| ) | ) | ||||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||||
| from authentik.stages.authenticator_validate.stage import ( | from authentik.stages.authenticator_validate.stage import ( | ||||||
|     SESSION_KEY_DEVICE_CHALLENGES, |     PLAN_CONTEXT_DEVICE_CHALLENGES, | ||||||
|     AuthenticatorValidateStageView, |     AuthenticatorValidateStageView, | ||||||
| ) | ) | ||||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | ||||||
| @ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|         plan.append_stage(stage) |         plan.append_stage(stage) | ||||||
|         plan.append_stage(UserLoginStage(name=generate_id())) |         plan.append_stage(UserLoginStage(name=generate_id())) | ||||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user |         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||||
|         session[SESSION_KEY_PLAN] = plan |         plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ | ||||||
|         session[SESSION_KEY_DEVICE_CHALLENGES] = [ |  | ||||||
|             { |             { | ||||||
|                 "device_class": device.__class__.__name__.lower().replace("device", ""), |                 "device_class": device.__class__.__name__.lower().replace("device", ""), | ||||||
|                 "device_uid": device.pk, |                 "device_uid": device.pk, | ||||||
|                 "challenge": {}, |                 "challenge": {}, | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( |         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" |             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||||
|         ) |         ) | ||||||
| @ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) |         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||||
|         plan.append_stage(stage) |         plan.append_stage(stage) | ||||||
|         plan.append_stage(UserLoginStage(name=generate_id())) |         plan.append_stage(UserLoginStage(name=generate_id())) | ||||||
|         session[SESSION_KEY_PLAN] = plan |         plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ | ||||||
|         session[SESSION_KEY_DEVICE_CHALLENGES] = [ |  | ||||||
|             { |             { | ||||||
|                 "device_class": device.__class__.__name__.lower().replace("device", ""), |                 "device_class": device.__class__.__name__.lower().replace("device", ""), | ||||||
|                 "device_uid": device.pk, |                 "device_uid": device.pk, | ||||||
|                 "challenge": {}, |                 "challenge": {}, | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( |         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" |             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.3} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.4} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -50,7 +50,7 @@ services: | |||||||
|       - "${COMPOSE_PORT_HTTP:-9000}:9000" |       - "${COMPOSE_PORT_HTTP:-9000}:9000" | ||||||
|       - "${COMPOSE_PORT_HTTPS:-9443}:9443" |       - "${COMPOSE_PORT_HTTPS:-9443}:9443" | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.3} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.4} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2023.5.3" | const VERSION = "2023.5.4" | ||||||
|  | |||||||
| @ -113,7 +113,7 @@ filterwarnings = [ | |||||||
|  |  | ||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2023.5.3" | version = "2023.5.4" | ||||||
| description = "" | description = "" | ||||||
| authors = ["authentik Team <hello@goauthentik.io>"] | authors = ["authentik Team <hello@goauthentik.io>"] | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2023.5.3 |   version: 2023.5.4 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
| @ -4783,6 +4783,38 @@ paths: | |||||||
|               schema: |               schema: | ||||||
|                 $ref: '#/components/schemas/GenericError' |                 $ref: '#/components/schemas/GenericError' | ||||||
|           description: '' |           description: '' | ||||||
|  |   /core/users/{id}/impersonate/: | ||||||
|  |     post: | ||||||
|  |       operationId: core_users_impersonate_create | ||||||
|  |       description: Impersonate a user | ||||||
|  |       parameters: | ||||||
|  |       - in: path | ||||||
|  |         name: id | ||||||
|  |         schema: | ||||||
|  |           type: integer | ||||||
|  |         description: A unique integer value identifying this User. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - core | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '204': | ||||||
|  |           description: Successfully started impersonation | ||||||
|  |         '401': | ||||||
|  |           description: Access denied | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|   /core/users/{id}/metrics/: |   /core/users/{id}/metrics/: | ||||||
|     get: |     get: | ||||||
|       operationId: core_users_metrics_retrieve |       operationId: core_users_metrics_retrieve | ||||||
| @ -4962,6 +4994,29 @@ paths: | |||||||
|               schema: |               schema: | ||||||
|                 $ref: '#/components/schemas/GenericError' |                 $ref: '#/components/schemas/GenericError' | ||||||
|           description: '' |           description: '' | ||||||
|  |   /core/users/impersonate_end/: | ||||||
|  |     get: | ||||||
|  |       operationId: core_users_impersonate_end_retrieve | ||||||
|  |       description: End Impersonation a user | ||||||
|  |       tags: | ||||||
|  |       - core | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       responses: | ||||||
|  |         '204': | ||||||
|  |           description: Successfully started impersonation | ||||||
|  |         '400': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/ValidationError' | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           content: | ||||||
|  |             application/json: | ||||||
|  |               schema: | ||||||
|  |                 $ref: '#/components/schemas/GenericError' | ||||||
|  |           description: '' | ||||||
|   /core/users/me/: |   /core/users/me/: | ||||||
|     get: |     get: | ||||||
|       operationId: core_users_me_retrieve |       operationId: core_users_me_retrieve | ||||||
| @ -40493,12 +40548,6 @@ components: | |||||||
|       type: object |       type: object | ||||||
|       description: Get system information. |       description: Get system information. | ||||||
|       properties: |       properties: | ||||||
|         env: |  | ||||||
|           type: object |  | ||||||
|           additionalProperties: |  | ||||||
|             type: string |  | ||||||
|           description: Get Environment |  | ||||||
|           readOnly: true |  | ||||||
|         http_headers: |         http_headers: | ||||||
|           type: object |           type: object | ||||||
|           additionalProperties: |           additionalProperties: | ||||||
| @ -40552,7 +40601,6 @@ components: | |||||||
|           readOnly: true |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - embedded_outpost_host |       - embedded_outpost_host | ||||||
|       - env |  | ||||||
|       - http_headers |       - http_headers | ||||||
|       - http_host |       - http_host | ||||||
|       - http_is_secure |       - http_is_secure | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -17,7 +17,7 @@ | |||||||
|                 "@codemirror/theme-one-dark": "^6.1.2", |                 "@codemirror/theme-one-dark": "^6.1.2", | ||||||
|                 "@formatjs/intl-listformat": "^7.2.2", |                 "@formatjs/intl-listformat": "^7.2.2", | ||||||
|                 "@fortawesome/fontawesome-free": "^6.4.0", |                 "@fortawesome/fontawesome-free": "^6.4.0", | ||||||
|                 "@goauthentik/api": "^2023.5.0-1684333401", |                 "@goauthentik/api": "^2023.5.3-1687462221", | ||||||
|                 "@lingui/cli": "^4.1.2", |                 "@lingui/cli": "^4.1.2", | ||||||
|                 "@lingui/core": "^4.1.2", |                 "@lingui/core": "^4.1.2", | ||||||
|                 "@lingui/detect-locale": "^4.1.2", |                 "@lingui/detect-locale": "^4.1.2", | ||||||
| @ -2127,9 +2127,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@goauthentik/api": { |         "node_modules/@goauthentik/api": { | ||||||
|             "version": "2023.5.0-1684333401", |             "version": "2023.5.3-1687462221", | ||||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.5.0-1684333401.tgz", |             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.5.3-1687462221.tgz", | ||||||
|             "integrity": "sha512-lbLXgqhvI65w3uodsJMdGIvFvJUzpleC0M4QneiXH3rTNbufVZWP9WbLCRLynKzEateOf3XSgjQ322BeZBA2bA==" |             "integrity": "sha512-34LJCBVPOfdlIHhDPQEA7NS7mJvrKJKHSfa2HPQClyVM3o5us8Bp4yJKs2nm4hGime3rbZAwYYq7n75tccr7rQ==" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@hcaptcha/types": { |         "node_modules/@hcaptcha/types": { | ||||||
|             "version": "1.0.3", |             "version": "1.0.3", | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ | |||||||
|         "@codemirror/theme-one-dark": "^6.1.2", |         "@codemirror/theme-one-dark": "^6.1.2", | ||||||
|         "@formatjs/intl-listformat": "^7.2.2", |         "@formatjs/intl-listformat": "^7.2.2", | ||||||
|         "@fortawesome/fontawesome-free": "^6.4.0", |         "@fortawesome/fontawesome-free": "^6.4.0", | ||||||
|         "@goauthentik/api": "^2023.5.0-1684333401", |         "@goauthentik/api": "^2023.5.3-1687462221", | ||||||
|         "@lingui/cli": "^4.1.2", |         "@lingui/cli": "^4.1.2", | ||||||
|         "@lingui/core": "^4.1.2", |         "@lingui/core": "^4.1.2", | ||||||
|         "@lingui/detect-locale": "^4.1.2", |         "@lingui/detect-locale": "^4.1.2", | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | |||||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { AdminApi, SessionUser, Version } from "@goauthentik/api"; | import { AdminApi, CoreApi, SessionUser, Version } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| autoDetectLanguage(); | autoDetectLanguage(); | ||||||
|  |  | ||||||
| @ -175,10 +175,11 @@ export class AdminInterface extends Interface { | |||||||
|             ${this.user?.original |             ${this.user?.original | ||||||
|                 ? html`<ak-sidebar-item |                 ? html`<ak-sidebar-item | ||||||
|                       ?highlight=${true} |                       ?highlight=${true} | ||||||
|                       ?isAbsoluteLink=${true} |                       @click=${() => { | ||||||
|                       path=${`/-/impersonation/end/?back=${encodeURIComponent( |                           new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => { | ||||||
|                           `${window.location.pathname}#${window.location.hash}`, |                               window.location.reload(); | ||||||
|                       )}`} |                           }); | ||||||
|  |                       }} | ||||||
|                   > |                   > | ||||||
|                       <span slot="label" |                       <span slot="label" | ||||||
|                           >${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span |                           >${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span | ||||||
|  | |||||||
| @ -115,9 +115,8 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> { | |||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser"> | ||||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser"> |  | ||||||
|                 <ak-search-select |                 <ak-search-select | ||||||
|                     .fetchObjects=${async (query?: string): Promise<User[]> => { |                     .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||||
|                         const args: CoreUsersListRequest = { |                         const args: CoreUsersListRequest = { | ||||||
| @ -144,7 +143,6 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> { | |||||||
|                 > |                 > | ||||||
|                 </ak-search-select> |                 </ak-search-select> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             ${this.result ? this.renderResult() : html``} |             ${this.result ? this.renderResult() : html``}`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,9 +21,12 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal | ||||||
|             <ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}> |                 label=${t`Common Name`} | ||||||
|  |                 name="commonName" | ||||||
|  |                 ?required=${true} | ||||||
|  |             > | ||||||
|                 <input type="text" class="pf-c-form-control" required /> |                 <input type="text" class="pf-c-form-control" required /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             <ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName"> |             <ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName"> | ||||||
| @ -38,7 +41,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> { | |||||||
|                 ?required=${true} |                 ?required=${true} | ||||||
|             > |             > | ||||||
|                 <input class="pf-c-form-control" type="number" value="365" /> |                 <input class="pf-c-form-control" type="number" value="365" /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal>`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -87,15 +87,13 @@ export class FlowImportForm extends Form<Flow> { | |||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`Flow`} name="flow"> | ||||||
|             <ak-form-element-horizontal label=${t`Flow`} name="flow"> |  | ||||||
|                 <input type="file" value="" class="pf-c-form-control" /> |                 <input type="file" value="" class="pf-c-form-control" /> | ||||||
|                 <p class="pf-c-form__helper-text"> |                 <p class="pf-c-form__helper-text"> | ||||||
|                     ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`} |                     ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`} | ||||||
|                 </p> |                 </p> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             ${this.result ? this.renderResult() : html``} |             ${this.result ? this.renderResult() : html``}`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -46,41 +46,39 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> { | |||||||
|         return data; |         return data; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups"> | ||||||
|             <ak-form-element-horizontal label=${t`Groups to add`} name="groups"> |             <div class="pf-c-input-group"> | ||||||
|                 <div class="pf-c-input-group"> |                 <ak-user-group-select-table | ||||||
|                     <ak-user-group-select-table |                     .confirm=${(items: Group[]) => { | ||||||
|                         .confirm=${(items: Group[]) => { |                         this.groupsToAdd = items; | ||||||
|                             this.groupsToAdd = items; |                         this.requestUpdate(); | ||||||
|                             this.requestUpdate(); |                         return Promise.resolve(); | ||||||
|                             return Promise.resolve(); |                     }} | ||||||
|                         }} |                 > | ||||||
|                     > |                     <button slot="trigger" class="pf-c-button pf-m-control" type="button"> | ||||||
|                         <button slot="trigger" class="pf-c-button pf-m-control" type="button"> |                         <i class="fas fa-plus" aria-hidden="true"></i> | ||||||
|                             <i class="fas fa-plus" aria-hidden="true"></i> |                     </button> | ||||||
|                         </button> |                 </ak-user-group-select-table> | ||||||
|                     </ak-user-group-select-table> |                 <div class="pf-c-form-control"> | ||||||
|                     <div class="pf-c-form-control"> |                     <ak-chip-group> | ||||||
|                         <ak-chip-group> |                         ${this.groupsToAdd.map((group) => { | ||||||
|                             ${this.groupsToAdd.map((group) => { |                             return html`<ak-chip | ||||||
|                                 return html`<ak-chip |                                 .removable=${true} | ||||||
|                                     .removable=${true} |                                 value=${ifDefined(group.pk)} | ||||||
|                                     value=${ifDefined(group.pk)} |                                 @remove=${() => { | ||||||
|                                     @remove=${() => { |                                     const idx = this.groupsToAdd.indexOf(group); | ||||||
|                                         const idx = this.groupsToAdd.indexOf(group); |                                     this.groupsToAdd.splice(idx, 1); | ||||||
|                                         this.groupsToAdd.splice(idx, 1); |                                     this.requestUpdate(); | ||||||
|                                         this.requestUpdate(); |                                 }} | ||||||
|                                     }} |                             > | ||||||
|                                 > |                                 ${group.name} | ||||||
|                                     ${group.name} |                             </ak-chip>`; | ||||||
|                                 </ak-chip>`; |                         })} | ||||||
|                             })} |                     </ak-chip-group> | ||||||
|                         </ak-chip-group> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |                 </div> | ||||||
|             </ak-form-element-horizontal> |             </div> | ||||||
|         </form> `; |         </ak-form-element-horizontal>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -116,9 +116,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | |||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> |  | ||||||
|                 <ak-search-select |                 <ak-search-select | ||||||
|                     .fetchObjects=${async (query?: string): Promise<User[]> => { |                     .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||||
|                         const args: CoreUsersListRequest = { |                         const args: CoreUsersListRequest = { | ||||||
| @ -155,7 +154,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | |||||||
|                     ${t`Set custom attributes using YAML or JSON.`} |                     ${t`Set custom attributes using YAML or JSON.`} | ||||||
|                 </p> |                 </p> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             ${this.result ? this.renderResult() : html``} |             ${this.result ? this.renderResult() : html``}`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -119,9 +119,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | |||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> |  | ||||||
|                 <ak-search-select |                 <ak-search-select | ||||||
|                     .fetchObjects=${async (query?: string): Promise<User[]> => { |                     .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||||
|                         const args: CoreUsersListRequest = { |                         const args: CoreUsersListRequest = { | ||||||
| @ -156,7 +155,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | |||||||
|                 </ak-codemirror> |                 </ak-codemirror> | ||||||
|                 <p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p> |                 <p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             ${this.result ? this.renderResult() : html``} |             ${this.result ? this.renderResult() : html``}`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -37,9 +37,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> |  | ||||||
|                 <input type="text" class="pf-c-form-control" required /> |                 <input type="text" class="pf-c-form-control" required /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             <ak-form-element-horizontal |             <ak-form-element-horizontal | ||||||
| @ -77,7 +76,6 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> { | |||||||
|  |  | ||||||
|             <ak-form-element-horizontal label=${t`Metadata`} name="metadata"> |             <ak-form-element-horizontal label=${t`Metadata`} name="metadata"> | ||||||
|                 <input type="file" value="" class="pf-c-form-control" /> |                 <input type="file" value="" class="pf-c-form-control" /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal>`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -59,9 +59,8 @@ export class RelatedUserAdd extends Form<{ users: number[] }> { | |||||||
|         return data; |         return data; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`${this.group?.isSuperuser ? html`` : html``} | ||||||
|             ${this.group?.isSuperuser ? html`` : html``} |  | ||||||
|             <ak-form-element-horizontal label=${t`Users to add`} name="users"> |             <ak-form-element-horizontal label=${t`Users to add`} name="users"> | ||||||
|                 <div class="pf-c-input-group"> |                 <div class="pf-c-input-group"> | ||||||
|                     <ak-group-member-select-table |                     <ak-group-member-select-table | ||||||
| @ -93,8 +92,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> { | |||||||
|                         </ak-chip-group> |                         </ak-chip-group> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal>`; | ||||||
|         </form> `; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -193,12 +191,20 @@ export class RelatedUserList extends Table<User> { | |||||||
|                 </ak-forms-modal> |                 </ak-forms-modal> | ||||||
|                 ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) |                 ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) | ||||||
|                     ? html` |                     ? html` | ||||||
|                           <a |                           <ak-action-button | ||||||
|                               class="pf-c-button pf-m-tertiary" |                               class="pf-m-tertiary" | ||||||
|                               href="${`/-/impersonation/${item.pk}/`}" |                               .apiRequest=${() => { | ||||||
|  |                                   return new CoreApi(DEFAULT_CONFIG) | ||||||
|  |                                       .coreUsersImpersonateCreate({ | ||||||
|  |                                           id: item.pk, | ||||||
|  |                                       }) | ||||||
|  |                                       .then(() => { | ||||||
|  |                                           window.location.href = "/"; | ||||||
|  |                                       }); | ||||||
|  |                               }} | ||||||
|                           > |                           > | ||||||
|                               ${t`Impersonate`} |                               ${t`Impersonate`} | ||||||
|                           </a> |                           </ak-action-button> | ||||||
|                       ` |                       ` | ||||||
|                     : html``}`, |                     : html``}`, | ||||||
|         ]; |         ]; | ||||||
|  | |||||||
| @ -35,9 +35,8 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> { | |||||||
|         this.result = undefined; |         this.result = undefined; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderRequestForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name"> | ||||||
|             <ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name"> |  | ||||||
|                 <input type="text" value="" class="pf-c-form-control" required /> |                 <input type="text" value="" class="pf-c-form-control" required /> | ||||||
|                 <p class="pf-c-form__helper-text"> |                 <p class="pf-c-form__helper-text"> | ||||||
|                     ${t`User's primary identifier. 150 characters or fewer.`} |                     ${t`User's primary identifier. 150 characters or fewer.`} | ||||||
| @ -78,8 +77,7 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> { | |||||||
|                     value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}" |                     value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}" | ||||||
|                     class="pf-c-form-control" |                     class="pf-c-form-control" | ||||||
|                 /> |                 /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal>`; | ||||||
|         </form>`; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderResponseForm(): TemplateResult { |     renderResponseForm(): TemplateResult { | ||||||
| @ -113,6 +111,6 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> { | |||||||
|         if (this.result) { |         if (this.result) { | ||||||
|             return this.renderResponseForm(); |             return this.renderResponseForm(); | ||||||
|         } |         } | ||||||
|         return this.renderRequestForm(); |         return super.renderForm(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -196,12 +196,20 @@ export class UserListPage extends TablePage<User> { | |||||||
|                 </ak-forms-modal> |                 </ak-forms-modal> | ||||||
|                 ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) |                 ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) | ||||||
|                     ? html` |                     ? html` | ||||||
|                           <a |                           <ak-action-button | ||||||
|                               class="pf-c-button pf-m-tertiary" |                               class="pf-m-tertiary" | ||||||
|                               href="${`/-/impersonation/${item.pk}/`}" |                               .apiRequest=${() => { | ||||||
|  |                                   return new CoreApi(DEFAULT_CONFIG) | ||||||
|  |                                       .coreUsersImpersonateCreate({ | ||||||
|  |                                           id: item.pk, | ||||||
|  |                                       }) | ||||||
|  |                                       .then(() => { | ||||||
|  |                                           window.location.href = "/"; | ||||||
|  |                                       }); | ||||||
|  |                               }} | ||||||
|                           > |                           > | ||||||
|                               ${t`Impersonate`} |                               ${t`Impersonate`} | ||||||
|                           </a> |                           </ak-action-button> | ||||||
|                       ` |                       ` | ||||||
|                     : html``}`, |                     : html``}`, | ||||||
|         ]; |         ]; | ||||||
|  | |||||||
| @ -26,11 +26,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal | ||||||
|             <ak-form-element-horizontal label=${t`Password`} ?required=${true} name="password"> |             label=${t`Password`} | ||||||
|                 <input type="password" value="" class="pf-c-form-control" required /> |             ?required=${true} | ||||||
|             </ak-form-element-horizontal> |             name="password" | ||||||
|         </form>`; |         > | ||||||
|  |             <input type="password" value="" class="pf-c-form-control" required /> | ||||||
|  |         </ak-form-element-horizontal>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -32,32 +32,34 @@ export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveReque | |||||||
|         return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data); |         return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderInlineForm(): TemplateResult { | ||||||
|         return html`<form class="pf-c-form pf-m-horizontal"> |         return html`<ak-form-element-horizontal | ||||||
|             <ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage"> |             label=${t`Email stage`} | ||||||
|                 <ak-search-select |             ?required=${true} | ||||||
|                     .fetchObjects=${async (query?: string): Promise<Stage[]> => { |             name="emailStage" | ||||||
|                         const args: StagesAllListRequest = { |         > | ||||||
|                             ordering: "name", |             <ak-search-select | ||||||
|                         }; |                 .fetchObjects=${async (query?: string): Promise<Stage[]> => { | ||||||
|                         if (query !== undefined) { |                     const args: StagesAllListRequest = { | ||||||
|                             args.search = query; |                         ordering: "name", | ||||||
|                         } |                     }; | ||||||
|                         const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args); |                     if (query !== undefined) { | ||||||
|                         return stages.results; |                         args.search = query; | ||||||
|                     }} |                     } | ||||||
|                     .groupBy=${(items: Stage[]) => { |                     const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args); | ||||||
|                         return groupBy(items, (stage) => stage.verboseNamePlural); |                     return stages.results; | ||||||
|                     }} |                 }} | ||||||
|                     .renderElement=${(stage: Stage): string => { |                 .groupBy=${(items: Stage[]) => { | ||||||
|                         return stage.name; |                     return groupBy(items, (stage) => stage.verboseNamePlural); | ||||||
|                     }} |                 }} | ||||||
|                     .value=${(stage: Stage | undefined): string | undefined => { |                 .renderElement=${(stage: Stage): string => { | ||||||
|                         return stage?.pk; |                     return stage.name; | ||||||
|                     }} |                 }} | ||||||
|                 > |                 .value=${(stage: Stage | undefined): string | undefined => { | ||||||
|                 </ak-search-select> |                     return stage?.pk; | ||||||
|             </ak-form-element-horizontal> |                 }} | ||||||
|         </form>`; |             > | ||||||
|  |             </ak-search-select> | ||||||
|  |         </ak-form-element-horizontal>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -201,12 +201,20 @@ export class UserViewPage extends AKElement { | |||||||
|                         ) |                         ) | ||||||
|                             ? html` |                             ? html` | ||||||
|                                   <div class="pf-c-card__footer"> |                                   <div class="pf-c-card__footer"> | ||||||
|                                       <a |                                       <ak-action-button | ||||||
|                                           class="pf-c-button pf-m-tertiary" |                                           class="pf-m-tertiary" | ||||||
|                                           href="${`/-/impersonation/${this.user?.pk}/`}" |                                           .apiRequest=${() => { | ||||||
|  |                                               return new CoreApi(DEFAULT_CONFIG) | ||||||
|  |                                                   .coreUsersImpersonateCreate({ | ||||||
|  |                                                       id: this.user?.pk || 0, | ||||||
|  |                                                   }) | ||||||
|  |                                                   .then(() => { | ||||||
|  |                                                       window.location.href = "/"; | ||||||
|  |                                                   }); | ||||||
|  |                                           }} | ||||||
|                                       > |                                       > | ||||||
|                                           ${t`Impersonate`} |                                           ${t`Impersonate`} | ||||||
|                                       </a> |                                       </ak-action-button> | ||||||
|                                   </div> |                                   </div> | ||||||
|                               ` |                               ` | ||||||
|                             : html``} |                             : html``} | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2023.5.3"; | export const VERSION = "2023.5.4"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -46,6 +46,7 @@ export class Diagram extends AKElement { | |||||||
|             flowchart: { |             flowchart: { | ||||||
|                 curve: "linear", |                 curve: "linear", | ||||||
|             }, |             }, | ||||||
|  |             htmlLabels: false, | ||||||
|         }; |         }; | ||||||
|         mermaid.initialize(this.config); |         mermaid.initialize(this.config); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -283,9 +283,23 @@ export abstract class Form<T> extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderForm(): TemplateResult { | ||||||
|  |         const inline = this.renderInlineForm(); | ||||||
|  |         if (inline) { | ||||||
|  |             return html`<form class="pf-c-form pf-m-horizontal" @submit=${this.submit}> | ||||||
|  |                 ${inline} | ||||||
|  |             </form>`; | ||||||
|  |         } | ||||||
|         return html`<slot></slot>`; |         return html`<slot></slot>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Inline form render callback when inheriting this class, should be overwritten | ||||||
|  |      * instead of `this.renderForm` | ||||||
|  |      */ | ||||||
|  |     renderInlineForm(): TemplateResult | undefined { | ||||||
|  |         return undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     renderNonFieldErrors(): TemplateResult { |     renderNonFieldErrors(): TemplateResult { | ||||||
|         if (!this.nonFieldErrors) { |         if (!this.nonFieldErrors) { | ||||||
|             return html``; |             return html``; | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import { me } from "@goauthentik/common/users"; | |||||||
| import { first } from "@goauthentik/common/utils"; | import { first } from "@goauthentik/common/utils"; | ||||||
| import { WebsocketClient } from "@goauthentik/common/ws"; | import { WebsocketClient } from "@goauthentik/common/ws"; | ||||||
| import { Interface } from "@goauthentik/elements/Base"; | import { Interface } from "@goauthentik/elements/Base"; | ||||||
|  | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/messages/MessageContainer"; | import "@goauthentik/elements/messages/MessageContainer"; | ||||||
| import "@goauthentik/elements/notifications/APIDrawer"; | import "@goauthentik/elements/notifications/APIDrawer"; | ||||||
| import "@goauthentik/elements/notifications/NotificationDrawer"; | import "@goauthentik/elements/notifications/NotificationDrawer"; | ||||||
| @ -36,7 +37,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css"; | |||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
| import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; | import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; | ||||||
|  |  | ||||||
| import { EventsApi, SessionUser } from "@goauthentik/api"; | import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| autoDetectLanguage(); | autoDetectLanguage(); | ||||||
|  |  | ||||||
| @ -234,18 +235,23 @@ export class UserInterface extends Interface { | |||||||
|                             : html``} |                             : html``} | ||||||
|                     </div> |                     </div> | ||||||
|                     ${this.me.original |                     ${this.me.original | ||||||
|                         ? html`<div class="pf-c-page__header-tools"> |                         ? html`  | ||||||
|                               <div class="pf-c-page__header-tools-group"> |                               <div class="pf-c-page__header-tools"> | ||||||
|                                   <a |                                   <div class="pf-c-page__header-tools-group"> | ||||||
|                                       class="pf-c-button pf-m-warning pf-m-small" |                                       <ak-action-button | ||||||
|                                       href=${`/-/impersonation/end/?back=${encodeURIComponent( |                                           class="pf-m-warning pf-m-small" | ||||||
|                                           `${window.location.pathname}#${window.location.hash}`, |                                           .apiRequest=${() => { | ||||||
|                                       )}`} |                                               return new CoreApi(DEFAULT_CONFIG) | ||||||
|                                   > |                                                   .coreUsersImpersonateEndRetrieve() | ||||||
|                                       ${t`Stop impersonation`} |                                                   .then(() => { | ||||||
|                                   </a> |                                                       window.location.reload(); | ||||||
|                               </div> |                                                   }); | ||||||
|                           </div>` |                                           }} | ||||||
|  |                                       > | ||||||
|  |                                           ${t`Stop impersonation`} | ||||||
|  |                                       </ak-action-button> | ||||||
|  |                                   </div> | ||||||
|  |                               </div>` | ||||||
|                         : html``} |                         : html``} | ||||||
|                     <div class="pf-c-page__header-tools-group"> |                     <div class="pf-c-page__header-tools-group"> | ||||||
|                         <div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"> |                         <div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"> | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	