Compare commits
	
		
			16 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e1bae1240f | |||
| 37bd62d291 | |||
| ac63db0136 | |||
| 5cdf3a09a9 | |||
| 3e17adf33f | |||
| 8392916c84 | |||
| 7e75a48fd0 | |||
| d69d84e48c | |||
| 78cc8fa498 | |||
| 0fcdf5e968 | |||
| f05997740f | |||
| 1aff300171 | |||
| ffb98eaa75 | |||
| 5c1db432f0 | |||
| 07fd4daa3e | |||
| aa80babfff | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2023.4.0 | ||||
| current_version = 2023.4.2 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | ||||
|  | ||||
							
								
								
									
										7
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
								
							| @ -10,6 +10,11 @@ jobs: | ||||
|     name: Delete old unused container images | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - id: generate_token | ||||
|         uses: tibdex/github-app-token@v1 | ||||
|         with: | ||||
|           app_id: ${{ secrets.GH_APP_ID }} | ||||
|           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||
|       - name: Delete 'dev' containers older than a week | ||||
|         uses: snok/container-retention-policy@v2 | ||||
|         with: | ||||
| @ -18,5 +23,5 @@ jobs: | ||||
|           account-type: org | ||||
|           org-name: goauthentik | ||||
|           untagged-only: false | ||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           token: ${{ steps.generate_token.outputs.token }} | ||||
|           skip-tags: gh-next,gh-main | ||||
|  | ||||
							
								
								
									
										9
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -22,18 +22,23 @@ jobs: | ||||
|           docker-compose up --no-start | ||||
|           docker-compose start postgresql redis | ||||
|           docker-compose run -u root server test-all | ||||
|       - id: generate_token | ||||
|         uses: tibdex/github-app-token@v1 | ||||
|         with: | ||||
|           app_id: ${{ secrets.GH_APP_ID }} | ||||
|           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||
|       - name: Extract version number | ||||
|         id: get_version | ||||
|         uses: actions/github-script@v6 | ||||
|         with: | ||||
|           github-token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           github-token: ${{ steps.generate_token.outputs.token }} | ||||
|           script: | | ||||
|             return context.payload.ref.replace(/\/refs\/tags\/version\//, ''); | ||||
|       - name: Create Release | ||||
|         id: create_release | ||||
|         uses: actions/create-release@v1.1.4 | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} | ||||
|         with: | ||||
|           tag_name: ${{ github.ref }} | ||||
|           release_name: Release ${{ steps.get_version.outputs.result }} | ||||
|  | ||||
							
								
								
									
										9
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							| @ -18,9 +18,14 @@ jobs: | ||||
|   compile: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - id: generate_token | ||||
|         uses: tibdex/github-app-token@v1 | ||||
|         with: | ||||
|           app_id: ${{ secrets.GH_APP_ID }} | ||||
|           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||
|       - uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           token: ${{ steps.generate_token.outputs.token }} | ||||
|       - name: Setup authentik env | ||||
|         uses: ./.github/actions/setup | ||||
|       - name: run compile | ||||
| @ -29,7 +34,7 @@ jobs: | ||||
|         uses: peter-evans/create-pull-request@v5 | ||||
|         id: cpr | ||||
|         with: | ||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           token: ${{ steps.generate_token.outputs.token }} | ||||
|           branch: compile-backend-translation | ||||
|           commit-message: "core: compile backend translations" | ||||
|           title: "core: compile backend translations" | ||||
|  | ||||
							
								
								
									
										11
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -9,9 +9,14 @@ jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - id: generate_token | ||||
|         uses: tibdex/github-app-token@v1 | ||||
|         with: | ||||
|           app_id: ${{ secrets.GH_APP_ID }} | ||||
|           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||
|       - uses: actions/checkout@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           token: ${{ steps.generate_token.outputs.token }} | ||||
|       - uses: actions/setup-node@v3.6.0 | ||||
|         with: | ||||
|           node-version: '18' | ||||
| @ -33,7 +38,7 @@ jobs: | ||||
|       - uses: peter-evans/create-pull-request@v5 | ||||
|         id: cpr | ||||
|         with: | ||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           token: ${{ steps.generate_token.outputs.token }} | ||||
|           branch: update-web-api-client | ||||
|           commit-message: "web: bump API Client version" | ||||
|           title: "web: bump API Client version" | ||||
| @ -44,6 +49,6 @@ jobs: | ||||
|           author: authentik bot <github-bot@goauthentik.io> | ||||
|       - uses: peter-evans/enable-pull-request-automerge@v3 | ||||
|         with: | ||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} | ||||
|           token: ${{ steps.generate_token.outputs.token }} | ||||
|           pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} | ||||
|           merge-method: squash | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| from os import environ | ||||
| from typing import Optional | ||||
|  | ||||
| __version__ = "2023.4.0" | ||||
| __version__ = "2023.4.2" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| """authentik administration overview""" | ||||
| import os | ||||
| import platform | ||||
| from datetime import datetime | ||||
| from sys import version as python_version | ||||
| @ -34,7 +33,6 @@ class RuntimeDict(TypedDict): | ||||
| class SystemSerializer(PassiveSerializer): | ||||
|     """Get system information.""" | ||||
|  | ||||
|     env = SerializerMethodField() | ||||
|     http_headers = SerializerMethodField() | ||||
|     http_host = SerializerMethodField() | ||||
|     http_is_secure = SerializerMethodField() | ||||
| @ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer): | ||||
|     server_time = 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]: | ||||
|         """Get HTTP Request headers""" | ||||
|         headers = {} | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """API Authentication""" | ||||
| from hmac import compare_digest | ||||
| from typing import Any, Optional | ||||
|  | ||||
| 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""" | ||||
|     from authentik.outposts.apps import MANAGED_OUTPOST | ||||
|  | ||||
|     if value != settings.SECRET_KEY: | ||||
|     if not compare_digest(value, settings.SECRET_KEY): | ||||
|         return None | ||||
|     outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) | ||||
|     if not outposts: | ||||
|  | ||||
| @ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): | ||||
|     def retrieve_file(self) -> str: | ||||
|         """Get blueprint from path""" | ||||
|         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: | ||||
|                 return _file.read() | ||||
|         except (IOError, OSError) as exc: | ||||
|  | ||||
| @ -1,34 +1,15 @@ | ||||
| """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 | ||||
| from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
|  | ||||
| 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)) | ||||
|     def test_retrieve_file(self): | ||||
|         """Test retrieve_file""" | ||||
|         instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts") | ||||
|         with self.assertRaises(BlueprintRetrievalFailed): | ||||
|             instance.retrieve() | ||||
|  | ||||
							
								
								
									
										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, | ||||
|     User, | ||||
| ) | ||||
| from authentik.events.models import EventAction | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.exceptions import FlowNonApplicableException | ||||
| from authentik.flows.models import FlowToken | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||
| 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.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| @ -543,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         send_mails(email_stage, message) | ||||
|         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: | ||||
|         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||
|         for backend in list(self.filter_backends): | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| """impersonation tests""" | ||||
| from json import loads | ||||
|  | ||||
| from django.test.testcases import TestCase | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
|  | ||||
|  | ||||
| class TestImpersonation(TestCase): | ||||
| class TestImpersonation(APITestCase): | ||||
|     """impersonation tests""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
| @ -23,10 +23,10 @@ class TestImpersonation(TestCase): | ||||
|         self.other_user.save() | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|         self.client.get( | ||||
|         self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_core:impersonate-init", | ||||
|                 kwargs={"user_id": self.other_user.pk}, | ||||
|                 "authentik_api:user-impersonate", | ||||
|                 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["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_body = loads(response.content.decode()) | ||||
| @ -46,9 +46,7 @@ class TestImpersonation(TestCase): | ||||
|         """test impersonation without permissions""" | ||||
|         self.client.force_login(self.other_user) | ||||
|  | ||||
|         self.client.get( | ||||
|             reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk}) | ||||
|         ) | ||||
|         self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})) | ||||
|  | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         response_body = loads(response.content.decode()) | ||||
| @ -58,5 +56,5 @@ class TestImpersonation(TestCase): | ||||
|         """test un-impersonation without impersonating first""" | ||||
|         self.client.force_login(self.other_user) | ||||
|  | ||||
|         response = self.client.get(reverse("authentik_core:impersonate-end")) | ||||
|         self.assertRedirects(response, reverse("authentik_core:if-user")) | ||||
|         response = self.client.get(reverse("authentik_api:user-impersonate-end")) | ||||
|         self.assertEqual(response.status_code, 204) | ||||
|  | ||||
| @ -7,7 +7,7 @@ from django.urls import path | ||||
| from django.views.decorators.csrf import ensure_csrf_cookie | ||||
| from django.views.generic import RedirectView | ||||
|  | ||||
| from authentik.core.views import apps, impersonate | ||||
| from authentik.core.views import apps | ||||
| from authentik.core.views.debug import AccessDeniedView | ||||
| from authentik.core.views.interface import FlowInterfaceView, InterfaceView | ||||
| from authentik.core.views.session import EndSessionView | ||||
| @ -28,17 +28,6 @@ urlpatterns = [ | ||||
|         apps.RedirectToAppLaunch.as_view(), | ||||
|         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 | ||||
|     path( | ||||
|         "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: ["[", "]"]) | ||||
|  | ||||
|     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 != "": | ||||
|                 element = f"--{self.action}--> {element}" | ||||
|  | ||||
| @ -204,12 +204,12 @@ class ChallengeStageView(StageView): | ||||
|         for field, errors in response.errors.items(): | ||||
|             for error in errors: | ||||
|                 full_errors.setdefault(field, []) | ||||
|                 full_errors[field].append( | ||||
|                     { | ||||
|                         "string": str(error), | ||||
|                         "code": error.code, | ||||
|                     } | ||||
|                 ) | ||||
|                 field_error = { | ||||
|                     "string": str(error), | ||||
|                 } | ||||
|                 if hasattr(error, "code"): | ||||
|                     field_error["code"] = error.code | ||||
|                 full_errors[field].append(field_error) | ||||
|         challenge_response.initial_data["response_errors"] = full_errors | ||||
|         if not challenge_response.is_valid(): | ||||
|             self.logger.error( | ||||
|  | ||||
| @ -132,9 +132,9 @@ class TestPolicyProcess(TestCase): | ||||
|         ) | ||||
|         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.resolver_match = resolve(reverse("authentik_core:impersonate-end")) | ||||
|         http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end")) | ||||
|  | ||||
|         request = PolicyRequest(self.user) | ||||
|         request.set_http_request(http_request) | ||||
|  | ||||
| @ -63,8 +63,8 @@ def ldap_sync_password(sender, user: User, password: str, **_): | ||||
|     if not sources.exists(): | ||||
|         return | ||||
|     source = sources.first() | ||||
|     changer = LDAPPasswordChanger(source) | ||||
|     try: | ||||
|         changer = LDAPPasswordChanger(source) | ||||
|         changer.change_password(user, password) | ||||
|     except LDAPOperationResult as exc: | ||||
|         Event.new( | ||||
|  | ||||
| @ -134,6 +134,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) - | ||||
|     device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() | ||||
|     if not 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 | ||||
|  | ||||
|  | ||||
| @ -37,9 +37,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME | ||||
|  | ||||
| COOKIE_NAME_MFA = "authentik_mfa" | ||||
|  | ||||
| SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages" | ||||
| SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage" | ||||
| SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges" | ||||
| PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages" | ||||
| PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage" | ||||
| PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges" | ||||
|  | ||||
|  | ||||
| class SelectableStageSerializer(PassiveSerializer): | ||||
| @ -73,8 +73,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|     component = CharField(default="ak-stage-authenticator-validate") | ||||
|  | ||||
|     def _challenge_allowed(self, classes: list): | ||||
|         device_challenges: list[dict] = self.stage.request.session.get( | ||||
|             SESSION_KEY_DEVICE_CHALLENGES, [] | ||||
|         device_challenges: list[dict] = self.stage.executor.plan.context.get( | ||||
|             PLAN_CONTEXT_DEVICE_CHALLENGES, [] | ||||
|         ) | ||||
|         if not any(x["device_class"] in classes for x in device_challenges): | ||||
|             raise ValidationError("No compatible device class allowed") | ||||
| @ -104,7 +104,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|         """Check which challenge the user has selected. Actual logic only used for SMS stage.""" | ||||
|         # First check if the challenge is valid | ||||
|         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( | ||||
|                 "device_class", "" | ||||
|             ) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""): | ||||
| @ -122,11 +124,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|  | ||||
|     def validate_selected_stage(self, stage_pk: str) -> str: | ||||
|         """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): | ||||
|             raise ValidationError("Selected stage is invalid") | ||||
|         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 | ||||
|  | ||||
|     def validate(self, attrs: dict): | ||||
| @ -231,7 +233,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|             else: | ||||
|                 self.logger.debug("No pending user, continuing") | ||||
|                 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 | ||||
|         if len(challenges) < 1: | ||||
| @ -264,23 +266,23 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         if stage.configuration_stages.count() == 1: | ||||
|             next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk) | ||||
|             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 | ||||
|             # return it | ||||
|             self.executor.plan.insert_stage(next_stage) | ||||
|             return self.executor.stage_ok() | ||||
|         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) | ||||
|  | ||||
|     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         res = super().post(request, *args, **kwargs) | ||||
|         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 | ||||
|         ): | ||||
|             self.logger.debug("Got selected stage in session, running that") | ||||
|             stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE) | ||||
|             self.logger.debug("Got selected stage in context, running that") | ||||
|             stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE) | ||||
|             # Because the foreign key to stage.configuration_stage points to | ||||
|             # a base stage class, we need to do another lookup | ||||
|             stage = Stage.objects.get_subclass(pk=stage_pk) | ||||
| @ -291,8 +293,8 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|         return res | ||||
|  | ||||
|     def get_challenge(self) -> AuthenticatorValidationChallenge: | ||||
|         challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []) | ||||
|         stages = self.request.session.get(SESSION_KEY_STAGES, []) | ||||
|         challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, []) | ||||
|         stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, []) | ||||
|         stage_challenges = [] | ||||
|         for stage in stages: | ||||
|             serializer = SelectableStageSerializer( | ||||
| @ -307,6 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|             stage_challenges.append(serializer.data) | ||||
|         return AuthenticatorValidationChallenge( | ||||
|             data={ | ||||
|                 "component": "ak-stage-authenticator-validate", | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|                 "device_challenges": challenges, | ||||
|                 "configuration_stages": stage_challenges, | ||||
| @ -390,8 +393,3 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 ) | ||||
|             ) | ||||
|         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""" | ||||
| from unittest.mock import MagicMock, patch | ||||
|  | ||||
| from django.contrib.sessions.middleware import SessionMiddleware | ||||
| from django.test.client import RequestFactory | ||||
| 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.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction | ||||
| from authentik.flows.planner import FlowPlan | ||||
| from authentik.flows.stage import StageView | ||||
| 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.tests.utils import dummy_get_response | ||||
| from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | ||||
| from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer | ||||
| from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses | ||||
| from authentik.stages.authenticator_validate.stage import ( | ||||
|     SESSION_KEY_DEVICE_CHALLENGES, | ||||
|     AuthenticatorValidationChallengeResponse, | ||||
| ) | ||||
| from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES | ||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | ||||
|  | ||||
|  | ||||
| @ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase): | ||||
|  | ||||
|     def test_validate_selected_challenge(self): | ||||
|         """Test validate_selected_challenge""" | ||||
|         # Prepare request with session | ||||
|         request = self.request_factory.get("/") | ||||
|         flow = create_test_flow() | ||||
|         stage = AuthenticatorValidateStage.objects.create( | ||||
|             name=generate_id(), | ||||
|             not_configured_action=NotConfiguredAction.CONFIGURE, | ||||
|             device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP], | ||||
|         ) | ||||
|  | ||||
|         middleware = SessionMiddleware(dummy_get_response) | ||||
|         middleware.process_request(request) | ||||
|         request.session[SESSION_KEY_DEVICE_CHALLENGES] = [ | ||||
|         session = self.client.session | ||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||
|         plan.append_stage(stage) | ||||
|         plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ | ||||
|             { | ||||
|                 "device_class": "static", | ||||
|                 "device_uid": "1", | ||||
| @ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase): | ||||
|                 "device_uid": "2", | ||||
|             }, | ||||
|         ] | ||||
|         request.session.save() | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|  | ||||
|         res = AuthenticatorValidationChallengeResponse() | ||||
|         res.stage = StageView(FlowExecutorView()) | ||||
|         res.stage.request = request | ||||
|         with self.assertRaises(ValidationError): | ||||
|             res.validate_selected_challenge( | ||||
|                 { | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|             data={ | ||||
|                 "selected_challenge": { | ||||
|                     "device_class": "baz", | ||||
|                     "device_uid": "quox", | ||||
|                     "challenge": {}, | ||||
|                 } | ||||
|             ) | ||||
|         res.validate_selected_challenge( | ||||
|             { | ||||
|                 "device_class": "static", | ||||
|                 "device_uid": "1", | ||||
|             } | ||||
|             }, | ||||
|         ) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             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( | ||||
|  | ||||
| @ -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.stage import ( | ||||
|     SESSION_KEY_DEVICE_CHALLENGES, | ||||
|     PLAN_CONTEXT_DEVICE_CHALLENGES, | ||||
|     AuthenticatorValidateStageView, | ||||
| ) | ||||
| from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice | ||||
| @ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|         plan.append_stage(stage) | ||||
|         plan.append_stage(UserLoginStage(name=generate_id())) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_DEVICE_CHALLENGES] = [ | ||||
|         plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ | ||||
|             { | ||||
|                 "device_class": device.__class__.__name__.lower().replace("device", ""), | ||||
|                 "device_uid": device.pk, | ||||
|                 "challenge": {}, | ||||
|             } | ||||
|         ] | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||
|         ) | ||||
| @ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|         plan = FlowPlan(flow_pk=flow.pk.hex) | ||||
|         plan.append_stage(stage) | ||||
|         plan.append_stage(UserLoginStage(name=generate_id())) | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_DEVICE_CHALLENGES] = [ | ||||
|         plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [ | ||||
|             { | ||||
|                 "device_class": device.__class__.__name__.lower().replace("device", ""), | ||||
|                 "device_uid": device.pk, | ||||
|                 "challenge": {}, | ||||
|             } | ||||
|         ] | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes( | ||||
|             "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA" | ||||
|         ) | ||||
|  | ||||
| @ -32,7 +32,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.0} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.2} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -50,7 +50,7 @@ services: | ||||
|       - "${AUTHENTIK_PORT_HTTP:-9000}:9000" | ||||
|       - "${AUTHENTIK_PORT_HTTPS:-9443}:9443" | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.0} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.2} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
| @ -29,4 +29,4 @@ func UserAgent() string { | ||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2023.4.0" | ||||
| const VERSION = "2023.4.2" | ||||
|  | ||||
| @ -105,7 +105,7 @@ filterwarnings = [ | ||||
|  | ||||
| [tool.poetry] | ||||
| name = "authentik" | ||||
| version = "2023.4.0" | ||||
| version = "2023.4.2" | ||||
| description = "" | ||||
| authors = ["authentik Team <hello@goauthentik.io>"] | ||||
|  | ||||
|  | ||||
							
								
								
									
										64
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2023.4.0 | ||||
|   version: 2023.4.2 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     email: hello@goauthentik.io | ||||
| @ -4783,6 +4783,38 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           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/: | ||||
|     get: | ||||
|       operationId: core_users_metrics_retrieve | ||||
| @ -4962,6 +4994,29 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           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/: | ||||
|     get: | ||||
|       operationId: core_users_me_retrieve | ||||
| @ -40367,12 +40422,6 @@ components: | ||||
|       type: object | ||||
|       description: Get system information. | ||||
|       properties: | ||||
|         env: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
|             type: string | ||||
|           description: Get Environment | ||||
|           readOnly: true | ||||
|         http_headers: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40426,7 +40475,6 @@ components: | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - embedded_outpost_host | ||||
|       - env | ||||
|       - http_headers | ||||
|       - http_host | ||||
|       - http_is_secure | ||||
|  | ||||
							
								
								
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -22,7 +22,7 @@ | ||||
|                 "@codemirror/theme-one-dark": "^6.1.1", | ||||
|                 "@formatjs/intl-listformat": "^7.1.9", | ||||
|                 "@fortawesome/fontawesome-free": "^6.4.0", | ||||
|                 "@goauthentik/api": "^2023.4.0-1681471246", | ||||
|                 "@goauthentik/api": "^2023.4.1-1687461872", | ||||
|                 "@hcaptcha/types": "^1.0.3", | ||||
|                 "@jackfranklin/rollup-plugin-markdown": "^0.4.0", | ||||
|                 "@lingui/cli": "^3.17.2", | ||||
| @ -2026,9 +2026,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@goauthentik/api": { | ||||
|             "version": "2023.4.0-1681471246", | ||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.0-1681471246.tgz", | ||||
|             "integrity": "sha512-/P9CfSHM4qEe1eaphC5MTYb/4yVrXBqME2amrj9JtK8dItGM/qSGDMIS8v18zZUsbO5fM+RQ/AtL/Izj1COZWA==" | ||||
|             "version": "2023.4.1-1687461872", | ||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.1-1687461872.tgz", | ||||
|             "integrity": "sha512-pY6Lcxyw6K04MAZPJz8JPeQkyR8lFcZPeC/twmSegzVQjrP5ygvpj1WSzJildOQoDAn93jP1BU7oRRPEITgdLg==" | ||||
|         }, | ||||
|         "node_modules/@hcaptcha/types": { | ||||
|             "version": "1.0.3", | ||||
|  | ||||
| @ -66,7 +66,7 @@ | ||||
|         "@codemirror/theme-one-dark": "^6.1.1", | ||||
|         "@formatjs/intl-listformat": "^7.1.9", | ||||
|         "@fortawesome/fontawesome-free": "^6.4.0", | ||||
|         "@goauthentik/api": "^2023.4.0-1681471246", | ||||
|         "@goauthentik/api": "^2023.4.1-1687461872", | ||||
|         "@hcaptcha/types": "^1.0.3", | ||||
|         "@jackfranklin/rollup-plugin-markdown": "^0.4.0", | ||||
|         "@lingui/cli": "^3.17.2", | ||||
|  | ||||
| @ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | ||||
| import PFPage from "@patternfly/patternfly/components/Page/page.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(); | ||||
|  | ||||
| @ -175,10 +175,11 @@ export class AdminInterface extends Interface { | ||||
|             ${this.user?.original | ||||
|                 ? html`<ak-sidebar-item | ||||
|                       ?highlight=${true} | ||||
|                       ?isAbsoluteLink=${true} | ||||
|                       path=${`/-/impersonation/end/?back=${encodeURIComponent( | ||||
|                           `${window.location.pathname}#${window.location.hash}`, | ||||
|                       )}`} | ||||
|                       @click=${() => { | ||||
|                           new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => { | ||||
|                               window.location.reload(); | ||||
|                           }); | ||||
|                       }} | ||||
|                   > | ||||
|                       <span slot="label" | ||||
|                           >${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 { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser"> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser"> | ||||
|                 <ak-search-select | ||||
|                     .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||
|                         const args: CoreUsersListRequest = { | ||||
| @ -144,7 +143,6 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> { | ||||
|                 > | ||||
|                 </ak-search-select> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|             ${this.result ? this.renderResult() : html``}`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -21,9 +21,12 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> { | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal | ||||
|                 label=${t`Common Name`} | ||||
|                 name="commonName" | ||||
|                 ?required=${true} | ||||
|             > | ||||
|                 <input type="text" class="pf-c-form-control" required /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName"> | ||||
| @ -38,7 +41,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> { | ||||
|                 ?required=${true} | ||||
|             > | ||||
|                 <input class="pf-c-form-control" type="number" value="365" /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|             </ak-form-element-horizontal>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -87,15 +87,13 @@ export class FlowImportForm extends Form<Flow> { | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Flow`} name="flow"> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`Flow`} name="flow"> | ||||
|                 <input type="file" value="" class="pf-c-form-control" /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|             ${this.result ? this.renderResult() : html``}`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -46,41 +46,39 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> { | ||||
|         return data; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Groups to add`} name="groups"> | ||||
|                 <div class="pf-c-input-group"> | ||||
|                     <ak-user-group-select-table | ||||
|                         .confirm=${(items: Group[]) => { | ||||
|                             this.groupsToAdd = items; | ||||
|                             this.requestUpdate(); | ||||
|                             return Promise.resolve(); | ||||
|                         }} | ||||
|                     > | ||||
|                         <button slot="trigger" class="pf-c-button pf-m-control" type="button"> | ||||
|                             <i class="fas fa-plus" aria-hidden="true"></i> | ||||
|                         </button> | ||||
|                     </ak-user-group-select-table> | ||||
|                     <div class="pf-c-form-control"> | ||||
|                         <ak-chip-group> | ||||
|                             ${this.groupsToAdd.map((group) => { | ||||
|                                 return html`<ak-chip | ||||
|                                     .removable=${true} | ||||
|                                     value=${ifDefined(group.pk)} | ||||
|                                     @remove=${() => { | ||||
|                                         const idx = this.groupsToAdd.indexOf(group); | ||||
|                                         this.groupsToAdd.splice(idx, 1); | ||||
|                                         this.requestUpdate(); | ||||
|                                     }} | ||||
|                                 > | ||||
|                                     ${group.name} | ||||
|                                 </ak-chip>`; | ||||
|                             })} | ||||
|                         </ak-chip-group> | ||||
|                     </div> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups"> | ||||
|             <div class="pf-c-input-group"> | ||||
|                 <ak-user-group-select-table | ||||
|                     .confirm=${(items: Group[]) => { | ||||
|                         this.groupsToAdd = items; | ||||
|                         this.requestUpdate(); | ||||
|                         return Promise.resolve(); | ||||
|                     }} | ||||
|                 > | ||||
|                     <button slot="trigger" class="pf-c-button pf-m-control" type="button"> | ||||
|                         <i class="fas fa-plus" aria-hidden="true"></i> | ||||
|                     </button> | ||||
|                 </ak-user-group-select-table> | ||||
|                 <div class="pf-c-form-control"> | ||||
|                     <ak-chip-group> | ||||
|                         ${this.groupsToAdd.map((group) => { | ||||
|                             return html`<ak-chip | ||||
|                                 .removable=${true} | ||||
|                                 value=${ifDefined(group.pk)} | ||||
|                                 @remove=${() => { | ||||
|                                     const idx = this.groupsToAdd.indexOf(group); | ||||
|                                     this.groupsToAdd.splice(idx, 1); | ||||
|                                     this.requestUpdate(); | ||||
|                                 }} | ||||
|                             > | ||||
|                                 ${group.name} | ||||
|                             </ak-chip>`; | ||||
|                         })} | ||||
|                     </ak-chip-group> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form> `; | ||||
|             </div> | ||||
|         </ak-form-element-horizontal>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -116,9 +116,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||
|                 <ak-search-select | ||||
|                     .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||
|                         const args: CoreUsersListRequest = { | ||||
| @ -155,7 +154,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | ||||
|                     ${t`Set custom attributes using YAML or JSON.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|             ${this.result ? this.renderResult() : html``}`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -64,9 +64,63 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | ||||
|         </ak-form-element-horizontal>`; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||
|     renderExampleButtons(): TemplateResult { | ||||
|         const header = html`<p>${t`Example context data`}</p>`; | ||||
|         switch (this.mapping?.metaModelName) { | ||||
|             case "authentik_sources_ldap.ldappropertymapping": | ||||
|                 return html`${header}${this.renderExampleLDAP()}`; | ||||
|             default: | ||||
|                 return html``; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     renderExampleLDAP(): TemplateResult { | ||||
|         return html` | ||||
|             <button | ||||
|                 class="pf-c-button pf-m-secondary" | ||||
|                 role="button" | ||||
|                 @click=${() => { | ||||
|                     this.request = { | ||||
|                         user: this.request?.user || 0, | ||||
|                         context: { | ||||
|                             ldap: { | ||||
|                                 name: "test-user", | ||||
|                                 objectSid: "S-1-5-21-2611707862-2219215769-354220275-1137", | ||||
|                                 objectClass: "person", | ||||
|                                 displayName: "authentik test user", | ||||
|                                 sAMAccountName: "sAMAccountName", | ||||
|                                 distinguishedName: "cn=user,ou=users,dc=goauthentik,dc=io", | ||||
|                             }, | ||||
|                         }, | ||||
|                     }; | ||||
|                 }} | ||||
|             > | ||||
|                 ${t`Active Directory User`} | ||||
|             </button> | ||||
|             <button | ||||
|                 class="pf-c-button pf-m-secondary" | ||||
|                 role="button" | ||||
|                 @click=${() => { | ||||
|                     this.request = { | ||||
|                         user: this.request?.user || 0, | ||||
|                         context: { | ||||
|                             ldap: { | ||||
|                                 name: "test-group", | ||||
|                                 objectSid: "S-1-5-21-2611707862-2219215769-354220275-1137", | ||||
|                                 objectClass: "group", | ||||
|                                 distinguishedName: "cn=group,ou=groups,dc=goauthentik,dc=io", | ||||
|                             }, | ||||
|                         }, | ||||
|                     }; | ||||
|                 }} | ||||
|             > | ||||
|                 ${t`Active Directory Group`} | ||||
|             </button> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user"> | ||||
|                 <ak-search-select | ||||
|                     .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||
|                         const args: CoreUsersListRequest = { | ||||
| @ -98,7 +152,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> { | ||||
|                     >> | ||||
|                 </ak-codemirror> | ||||
|             </ak-form-element-horizontal> | ||||
|             ${this.result ? this.renderResult() : html``} | ||||
|         </form>`; | ||||
|             ${this.result ? this.renderResult() : html``}`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -37,9 +37,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> { | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> | ||||
|                 <input type="text" class="pf-c-form-control" required /> | ||||
|             </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"> | ||||
|                 <input type="file" value="" class="pf-c-form-control" /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|             </ak-form-element-horizontal>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -59,9 +59,8 @@ export class RelatedUserAdd extends Form<{ users: number[] }> { | ||||
|         return data; | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             ${this.group?.isSuperuser ? html`` : html``} | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`${this.group?.isSuperuser ? html`` : html``} | ||||
|             <ak-form-element-horizontal label=${t`Users to add`} name="users"> | ||||
|                 <div class="pf-c-input-group"> | ||||
|                     <ak-group-member-select-table | ||||
| @ -93,8 +92,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> { | ||||
|                         </ak-chip-group> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form> `; | ||||
|             </ak-form-element-horizontal>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -193,12 +191,20 @@ export class RelatedUserList extends Table<User> { | ||||
|                 </ak-forms-modal> | ||||
|                 ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate) | ||||
|                     ? html` | ||||
|                           <a | ||||
|                               class="pf-c-button pf-m-tertiary" | ||||
|                               href="${`/-/impersonation/${item.pk}/`}" | ||||
|                           <ak-action-button | ||||
|                               class="pf-m-tertiary" | ||||
|                               .apiRequest=${() => { | ||||
|                                   return new CoreApi(DEFAULT_CONFIG) | ||||
|                                       .coreUsersImpersonateCreate({ | ||||
|                                           id: item.pk, | ||||
|                                       }) | ||||
|                                       .then(() => { | ||||
|                                           window.location.href = "/"; | ||||
|                                       }); | ||||
|                               }} | ||||
|                           > | ||||
|                               ${t`Impersonate`} | ||||
|                           </a> | ||||
|                           </ak-action-button> | ||||
|                       ` | ||||
|                     : html``}`, | ||||
|         ]; | ||||
|  | ||||
| @ -35,9 +35,8 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> { | ||||
|         this.result = undefined; | ||||
|     } | ||||
|  | ||||
|     renderRequestForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name"> | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name"> | ||||
|                 <input type="text" value="" class="pf-c-form-control" required /> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${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))}" | ||||
|                     class="pf-c-form-control" | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|             </ak-form-element-horizontal>`; | ||||
|     } | ||||
|  | ||||
|     renderResponseForm(): TemplateResult { | ||||
| @ -113,6 +111,6 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> { | ||||
|         if (this.result) { | ||||
|             return this.renderResponseForm(); | ||||
|         } | ||||
|         return this.renderRequestForm(); | ||||
|         return super.renderForm(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -151,12 +151,20 @@ export class UserListPage extends TablePage<User> { | ||||
|                 </ak-forms-modal> | ||||
|                 ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate) | ||||
|                     ? html` | ||||
|                           <a | ||||
|                               class="pf-c-button pf-m-tertiary" | ||||
|                               href="${`/-/impersonation/${item.pk}/`}" | ||||
|                           <ak-action-button | ||||
|                               class="pf-m-tertiary" | ||||
|                               .apiRequest=${() => { | ||||
|                                   return new CoreApi(DEFAULT_CONFIG) | ||||
|                                       .coreUsersImpersonateCreate({ | ||||
|                                           id: item.pk, | ||||
|                                       }) | ||||
|                                       .then(() => { | ||||
|                                           window.location.href = "/"; | ||||
|                                       }); | ||||
|                               }} | ||||
|                           > | ||||
|                               ${t`Impersonate`} | ||||
|                           </a> | ||||
|                           </ak-action-button> | ||||
|                       ` | ||||
|                     : html``}`, | ||||
|         ]; | ||||
|  | ||||
| @ -26,11 +26,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> { | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Password`} ?required=${true} name="password"> | ||||
|                 <input type="password" value="" class="pf-c-form-control" required /> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal | ||||
|             label=${t`Password`} | ||||
|             ?required=${true} | ||||
|             name="password" | ||||
|         > | ||||
|             <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); | ||||
|     }; | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html`<form class="pf-c-form pf-m-horizontal"> | ||||
|             <ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage"> | ||||
|                 <ak-search-select | ||||
|                     .fetchObjects=${async (query?: string): Promise<Stage[]> => { | ||||
|                         const args: StagesAllListRequest = { | ||||
|                             ordering: "name", | ||||
|                         }; | ||||
|                         if (query !== undefined) { | ||||
|                             args.search = query; | ||||
|                         } | ||||
|                         const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args); | ||||
|                         return stages.results; | ||||
|                     }} | ||||
|                     .groupBy=${(items: Stage[]) => { | ||||
|                         return groupBy(items, (stage) => stage.verboseNamePlural); | ||||
|                     }} | ||||
|                     .renderElement=${(stage: Stage): string => { | ||||
|                         return stage.name; | ||||
|                     }} | ||||
|                     .value=${(stage: Stage | undefined): string | undefined => { | ||||
|                         return stage?.pk; | ||||
|                     }} | ||||
|                 > | ||||
|                 </ak-search-select> | ||||
|             </ak-form-element-horizontal> | ||||
|         </form>`; | ||||
|     renderInlineForm(): TemplateResult { | ||||
|         return html`<ak-form-element-horizontal | ||||
|             label=${t`Email stage`} | ||||
|             ?required=${true} | ||||
|             name="emailStage" | ||||
|         > | ||||
|             <ak-search-select | ||||
|                 .fetchObjects=${async (query?: string): Promise<Stage[]> => { | ||||
|                     const args: StagesAllListRequest = { | ||||
|                         ordering: "name", | ||||
|                     }; | ||||
|                     if (query !== undefined) { | ||||
|                         args.search = query; | ||||
|                     } | ||||
|                     const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args); | ||||
|                     return stages.results; | ||||
|                 }} | ||||
|                 .groupBy=${(items: Stage[]) => { | ||||
|                     return groupBy(items, (stage) => stage.verboseNamePlural); | ||||
|                 }} | ||||
|                 .renderElement=${(stage: Stage): string => { | ||||
|                     return stage.name; | ||||
|                 }} | ||||
|                 .value=${(stage: Stage | undefined): string | undefined => { | ||||
|                     return stage?.pk; | ||||
|                 }} | ||||
|             > | ||||
|             </ak-search-select> | ||||
|         </ak-form-element-horizontal>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -201,12 +201,20 @@ export class UserViewPage extends AKElement { | ||||
|                         ) | ||||
|                             ? html` | ||||
|                                   <div class="pf-c-card__footer"> | ||||
|                                       <a | ||||
|                                           class="pf-c-button pf-m-tertiary" | ||||
|                                           href="${`/-/impersonation/${this.user?.pk}/`}" | ||||
|                                       <ak-action-button | ||||
|                                           class="pf-m-tertiary" | ||||
|                                           .apiRequest=${() => { | ||||
|                                               return new CoreApi(DEFAULT_CONFIG) | ||||
|                                                   .coreUsersImpersonateCreate({ | ||||
|                                                       id: this.user?.pk || 0, | ||||
|                                                   }) | ||||
|                                                   .then(() => { | ||||
|                                                       window.location.href = "/"; | ||||
|                                                   }); | ||||
|                                           }} | ||||
|                                       > | ||||
|                                           ${t`Impersonate`} | ||||
|                                       </a> | ||||
|                                       </ak-action-button> | ||||
|                                   </div> | ||||
|                               ` | ||||
|                             : html``} | ||||
|  | ||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | ||||
| export const ERROR_CLASS = "pf-m-danger"; | ||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||
| export const CURRENT_CLASS = "pf-m-current"; | ||||
| export const VERSION = "2023.4.0"; | ||||
| export const VERSION = "2023.4.2"; | ||||
| export const TITLE_DEFAULT = "authentik"; | ||||
| export const ROUTE_SEPARATOR = ";"; | ||||
|  | ||||
|  | ||||
| @ -46,6 +46,7 @@ export class Diagram extends AKElement { | ||||
|             flowchart: { | ||||
|                 curve: "linear", | ||||
|             }, | ||||
|             htmlLabels: false, | ||||
|         }; | ||||
|         mermaid.initialize(this.config); | ||||
|     } | ||||
|  | ||||
| @ -279,9 +279,23 @@ export abstract class Form<T> extends AKElement { | ||||
|     } | ||||
|  | ||||
|     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>`; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Inline form render callback when inheriting this class, should be overwritten | ||||
|      * instead of `this.renderForm` | ||||
|      */ | ||||
|     renderInlineForm(): TemplateResult | undefined { | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
|     renderNonFieldErrors(): TemplateResult { | ||||
|         if (!this.nonFieldErrors) { | ||||
|             return html``; | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { me } from "@goauthentik/common/users"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import { WebsocketClient } from "@goauthentik/common/ws"; | ||||
| import { Interface } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/buttons/ActionButton"; | ||||
| import "@goauthentik/elements/messages/MessageContainer"; | ||||
| import "@goauthentik/elements/notifications/APIDrawer"; | ||||
| 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 PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; | ||||
|  | ||||
| import { EventsApi, SessionUser } from "@goauthentik/api"; | ||||
| import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api"; | ||||
|  | ||||
| autoDetectLanguage(); | ||||
|  | ||||
| @ -234,18 +235,23 @@ export class UserInterface extends Interface { | ||||
|                             : html``} | ||||
|                     </div> | ||||
|                     ${this.me.original | ||||
|                         ? html`<div class="pf-c-page__header-tools"> | ||||
|                               <div class="pf-c-page__header-tools-group"> | ||||
|                                   <a | ||||
|                                       class="pf-c-button pf-m-warning pf-m-small" | ||||
|                                       href=${`/-/impersonation/end/?back=${encodeURIComponent( | ||||
|                                           `${window.location.pathname}#${window.location.hash}`, | ||||
|                                       )}`} | ||||
|                                   > | ||||
|                                       ${t`Stop impersonation`} | ||||
|                                   </a> | ||||
|                               </div> | ||||
|                           </div>` | ||||
|                         ? html`  | ||||
|                               <div class="pf-c-page__header-tools"> | ||||
|                                   <div class="pf-c-page__header-tools-group"> | ||||
|                                       <ak-action-button | ||||
|                                           class="pf-m-warning pf-m-small" | ||||
|                                           .apiRequest=${() => { | ||||
|                                               return new CoreApi(DEFAULT_CONFIG) | ||||
|                                                   .coreUsersImpersonateEndRetrieve() | ||||
|                                                   .then(() => { | ||||
|                                                       window.location.reload(); | ||||
|                                                   }); | ||||
|                                           }} | ||||
|                                       > | ||||
|                                           ${t`Stop impersonation`} | ||||
|                                       </ak-action-button> | ||||
|                                   </div> | ||||
|                               </div>` | ||||
|                         : html``} | ||||
|                     <div class="pf-c-page__header-tools-group"> | ||||
|                         <div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	