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]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2023.4.0
 | 
					current_version = 2023.4.2
 | 
				
			||||||
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+)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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
 | 
					    name: Delete old unused container images
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    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
 | 
					      - name: Delete 'dev' containers older than a week
 | 
				
			||||||
        uses: snok/container-retention-policy@v2
 | 
					        uses: snok/container-retention-policy@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@ -18,5 +23,5 @@ jobs:
 | 
				
			|||||||
          account-type: org
 | 
					          account-type: org
 | 
				
			||||||
          org-name: goauthentik
 | 
					          org-name: goauthentik
 | 
				
			||||||
          untagged-only: false
 | 
					          untagged-only: false
 | 
				
			||||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
          skip-tags: gh-next,gh-main
 | 
					          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 up --no-start
 | 
				
			||||||
          docker-compose start postgresql redis
 | 
					          docker-compose start postgresql redis
 | 
				
			||||||
          docker-compose run -u root server test-all
 | 
					          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
 | 
					      - name: Extract version number
 | 
				
			||||||
        id: get_version
 | 
					        id: get_version
 | 
				
			||||||
        uses: actions/github-script@v6
 | 
					        uses: actions/github-script@v6
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          github-token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
          script: |
 | 
					          script: |
 | 
				
			||||||
            return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
 | 
					            return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
 | 
				
			||||||
      - name: Create Release
 | 
					      - name: Create Release
 | 
				
			||||||
        id: create_release
 | 
					        id: create_release
 | 
				
			||||||
        uses: actions/create-release@v1.1.4
 | 
					        uses: actions/create-release@v1.1.4
 | 
				
			||||||
        env:
 | 
					        env:
 | 
				
			||||||
          GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          tag_name: ${{ github.ref }}
 | 
					          tag_name: ${{ github.ref }}
 | 
				
			||||||
          release_name: Release ${{ steps.get_version.outputs.result }}
 | 
					          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:
 | 
					  compile:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    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
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
      - name: Setup authentik env
 | 
					      - name: Setup authentik env
 | 
				
			||||||
        uses: ./.github/actions/setup
 | 
					        uses: ./.github/actions/setup
 | 
				
			||||||
      - name: run compile
 | 
					      - name: run compile
 | 
				
			||||||
@ -29,7 +34,7 @@ jobs:
 | 
				
			|||||||
        uses: peter-evans/create-pull-request@v5
 | 
					        uses: peter-evans/create-pull-request@v5
 | 
				
			||||||
        id: cpr
 | 
					        id: cpr
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
          branch: compile-backend-translation
 | 
					          branch: compile-backend-translation
 | 
				
			||||||
          commit-message: "core: compile backend translations"
 | 
					          commit-message: "core: compile backend translations"
 | 
				
			||||||
          title: "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:
 | 
					  build:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    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
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
      - uses: actions/setup-node@v3.6.0
 | 
					      - uses: actions/setup-node@v3.6.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: '18'
 | 
					          node-version: '18'
 | 
				
			||||||
@ -33,7 +38,7 @@ jobs:
 | 
				
			|||||||
      - uses: peter-evans/create-pull-request@v5
 | 
					      - uses: peter-evans/create-pull-request@v5
 | 
				
			||||||
        id: cpr
 | 
					        id: cpr
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
          branch: update-web-api-client
 | 
					          branch: update-web-api-client
 | 
				
			||||||
          commit-message: "web: bump API Client version"
 | 
					          commit-message: "web: bump API Client version"
 | 
				
			||||||
          title: "web: bump API Client version"
 | 
					          title: "web: bump API Client version"
 | 
				
			||||||
@ -44,6 +49,6 @@ jobs:
 | 
				
			|||||||
          author: authentik bot <github-bot@goauthentik.io>
 | 
					          author: authentik bot <github-bot@goauthentik.io>
 | 
				
			||||||
      - uses: peter-evans/enable-pull-request-automerge@v3
 | 
					      - uses: peter-evans/enable-pull-request-automerge@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
					          token: ${{ steps.generate_token.outputs.token }}
 | 
				
			||||||
          pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
 | 
					          pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
 | 
				
			||||||
          merge-method: squash
 | 
					          merge-method: squash
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = "2023.4.0"
 | 
					__version__ = "2023.4.2"
 | 
				
			||||||
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)
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.urls import path
 | 
				
			|||||||
from django.views.decorators.csrf import ensure_csrf_cookie
 | 
					from django.views.decorators.csrf import ensure_csrf_cookie
 | 
				
			||||||
from django.views.generic import RedirectView
 | 
					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.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
 | 
				
			||||||
@ -28,17 +28,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)
 | 
				
			||||||
 | 
				
			|||||||
@ -63,8 +63,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:
 | 
				
			||||||
        Event.new(
 | 
					        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()
 | 
					    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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -37,9 +37,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):
 | 
				
			||||||
@ -73,8 +73,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")
 | 
				
			||||||
@ -104,7 +104,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", ""):
 | 
				
			||||||
@ -122,11 +124,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):
 | 
				
			||||||
@ -231,7 +233,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:
 | 
				
			||||||
@ -264,23 +266,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)
 | 
				
			||||||
@ -291,8 +293,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(
 | 
				
			||||||
@ -307,6 +309,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,
 | 
				
			||||||
@ -390,8 +393,3 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        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(
 | 
				
			||||||
            {
 | 
					            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_class": "static",
 | 
				
			||||||
                    "device_uid": "1",
 | 
					                    "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.4.0}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.2}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: server
 | 
					    command: server
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
@ -50,7 +50,7 @@ services:
 | 
				
			|||||||
      - "${AUTHENTIK_PORT_HTTP:-9000}:9000"
 | 
					      - "${AUTHENTIK_PORT_HTTP:-9000}:9000"
 | 
				
			||||||
      - "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
 | 
					      - "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
 | 
				
			||||||
  worker:
 | 
					  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
 | 
					    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.4.0"
 | 
					const VERSION = "2023.4.2"
 | 
				
			||||||
 | 
				
			|||||||
@ -105,7 +105,7 @@ filterwarnings = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[tool.poetry]
 | 
					[tool.poetry]
 | 
				
			||||||
name = "authentik"
 | 
					name = "authentik"
 | 
				
			||||||
version = "2023.4.0"
 | 
					version = "2023.4.2"
 | 
				
			||||||
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.4.0
 | 
					  version: 2023.4.2
 | 
				
			||||||
  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
 | 
				
			||||||
@ -40367,12 +40422,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:
 | 
				
			||||||
@ -40426,7 +40475,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
									
									
									
								
							@ -22,7 +22,7 @@
 | 
				
			|||||||
                "@codemirror/theme-one-dark": "^6.1.1",
 | 
					                "@codemirror/theme-one-dark": "^6.1.1",
 | 
				
			||||||
                "@formatjs/intl-listformat": "^7.1.9",
 | 
					                "@formatjs/intl-listformat": "^7.1.9",
 | 
				
			||||||
                "@fortawesome/fontawesome-free": "^6.4.0",
 | 
					                "@fortawesome/fontawesome-free": "^6.4.0",
 | 
				
			||||||
                "@goauthentik/api": "^2023.4.0-1681471246",
 | 
					                "@goauthentik/api": "^2023.4.1-1687461872",
 | 
				
			||||||
                "@hcaptcha/types": "^1.0.3",
 | 
					                "@hcaptcha/types": "^1.0.3",
 | 
				
			||||||
                "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
 | 
					                "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
 | 
				
			||||||
                "@lingui/cli": "^3.17.2",
 | 
					                "@lingui/cli": "^3.17.2",
 | 
				
			||||||
@ -2026,9 +2026,9 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@goauthentik/api": {
 | 
					        "node_modules/@goauthentik/api": {
 | 
				
			||||||
            "version": "2023.4.0-1681471246",
 | 
					            "version": "2023.4.1-1687461872",
 | 
				
			||||||
            "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.0-1681471246.tgz",
 | 
					            "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.1-1687461872.tgz",
 | 
				
			||||||
            "integrity": "sha512-/P9CfSHM4qEe1eaphC5MTYb/4yVrXBqME2amrj9JtK8dItGM/qSGDMIS8v18zZUsbO5fM+RQ/AtL/Izj1COZWA=="
 | 
					            "integrity": "sha512-pY6Lcxyw6K04MAZPJz8JPeQkyR8lFcZPeC/twmSegzVQjrP5ygvpj1WSzJildOQoDAn93jP1BU7oRRPEITgdLg=="
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "node_modules/@hcaptcha/types": {
 | 
					        "node_modules/@hcaptcha/types": {
 | 
				
			||||||
            "version": "1.0.3",
 | 
					            "version": "1.0.3",
 | 
				
			||||||
 | 
				
			|||||||
@ -66,7 +66,7 @@
 | 
				
			|||||||
        "@codemirror/theme-one-dark": "^6.1.1",
 | 
					        "@codemirror/theme-one-dark": "^6.1.1",
 | 
				
			||||||
        "@formatjs/intl-listformat": "^7.1.9",
 | 
					        "@formatjs/intl-listformat": "^7.1.9",
 | 
				
			||||||
        "@fortawesome/fontawesome-free": "^6.4.0",
 | 
					        "@fortawesome/fontawesome-free": "^6.4.0",
 | 
				
			||||||
        "@goauthentik/api": "^2023.4.0-1681471246",
 | 
					        "@goauthentik/api": "^2023.4.1-1687461872",
 | 
				
			||||||
        "@hcaptcha/types": "^1.0.3",
 | 
					        "@hcaptcha/types": "^1.0.3",
 | 
				
			||||||
        "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
 | 
					        "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
 | 
				
			||||||
        "@lingui/cli": "^3.17.2",
 | 
					        "@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 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,9 +46,8 @@ 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[]) => {
 | 
				
			||||||
@ -79,8 +78,7 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
 | 
				
			|||||||
                    </ak-chip-group>
 | 
					                    </ak-chip-group>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            </ak-form-element-horizontal>
 | 
					        </ak-form-element-horizontal>`;
 | 
				
			||||||
        </form> `;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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>`;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -64,9 +64,63 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
 | 
				
			|||||||
        </ak-form-element-horizontal>`;
 | 
					        </ak-form-element-horizontal>`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderForm(): TemplateResult {
 | 
					    renderExampleButtons(): TemplateResult {
 | 
				
			||||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
					        const header = html`<p>${t`Example context data`}</p>`;
 | 
				
			||||||
            <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
 | 
					        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
 | 
					                <ak-search-select
 | 
				
			||||||
                    .fetchObjects=${async (query?: string): Promise<User[]> => {
 | 
					                    .fetchObjects=${async (query?: string): Promise<User[]> => {
 | 
				
			||||||
                        const args: CoreUsersListRequest = {
 | 
					                        const args: CoreUsersListRequest = {
 | 
				
			||||||
@ -98,7 +152,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
 | 
				
			|||||||
                    >>
 | 
					                    >>
 | 
				
			||||||
                </ak-codemirror>
 | 
					                </ak-codemirror>
 | 
				
			||||||
            </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.Impersonate)
 | 
					                ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
 | 
				
			||||||
                    ? 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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -151,12 +151,20 @@ export class UserListPage extends TablePage<User> {
 | 
				
			|||||||
                </ak-forms-modal>
 | 
					                </ak-forms-modal>
 | 
				
			||||||
                ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
 | 
					                ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
 | 
				
			||||||
                    ? 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`}
 | 
				
			||||||
 | 
					            ?required=${true}
 | 
				
			||||||
 | 
					            name="password"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
            <input type="password" value="" class="pf-c-form-control" required />
 | 
					            <input type="password" value="" class="pf-c-form-control" required />
 | 
				
			||||||
            </ak-form-element-horizontal>
 | 
					        </ak-form-element-horizontal>`;
 | 
				
			||||||
        </form>`;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -32,9 +32,12 @@ 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`}
 | 
				
			||||||
 | 
					            ?required=${true}
 | 
				
			||||||
 | 
					            name="emailStage"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
            <ak-search-select
 | 
					            <ak-search-select
 | 
				
			||||||
                .fetchObjects=${async (query?: string): Promise<Stage[]> => {
 | 
					                .fetchObjects=${async (query?: string): Promise<Stage[]> => {
 | 
				
			||||||
                    const args: StagesAllListRequest = {
 | 
					                    const args: StagesAllListRequest = {
 | 
				
			||||||
@ -57,7 +60,6 @@ export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveReque
 | 
				
			|||||||
                }}
 | 
					                }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
            </ak-search-select>
 | 
					            </ak-search-select>
 | 
				
			||||||
            </ak-form-element-horizontal>
 | 
					        </ak-form-element-horizontal>`;
 | 
				
			||||||
        </form>`;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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.4.0";
 | 
					export const VERSION = "2023.4.2";
 | 
				
			||||||
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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -279,9 +279,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,16 +235,21 @@ 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">
 | 
				
			||||||
                                  <div class="pf-c-page__header-tools-group">
 | 
					                                  <div class="pf-c-page__header-tools-group">
 | 
				
			||||||
                                  <a
 | 
					                                      <ak-action-button
 | 
				
			||||||
                                      class="pf-c-button pf-m-warning pf-m-small"
 | 
					                                          class="pf-m-warning pf-m-small"
 | 
				
			||||||
                                      href=${`/-/impersonation/end/?back=${encodeURIComponent(
 | 
					                                          .apiRequest=${() => {
 | 
				
			||||||
                                          `${window.location.pathname}#${window.location.hash}`,
 | 
					                                              return new CoreApi(DEFAULT_CONFIG)
 | 
				
			||||||
                                      )}`}
 | 
					                                                  .coreUsersImpersonateEndRetrieve()
 | 
				
			||||||
 | 
					                                                  .then(() => {
 | 
				
			||||||
 | 
					                                                      window.location.reload();
 | 
				
			||||||
 | 
					                                                  });
 | 
				
			||||||
 | 
					                                          }}
 | 
				
			||||||
                                      >
 | 
					                                      >
 | 
				
			||||||
                                          ${t`Stop impersonation`}
 | 
					                                          ${t`Stop impersonation`}
 | 
				
			||||||
                                  </a>
 | 
					                                      </ak-action-button>
 | 
				
			||||||
                                  </div>
 | 
					                                  </div>
 | 
				
			||||||
                              </div>`
 | 
					                              </div>`
 | 
				
			||||||
                        : html``}
 | 
					                        : html``}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user