Compare commits
	
		
			1 Commits
		
	
	
		
			version-20
			...
			beta-upgra
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 406d18ead6 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 2023.5.6
 | 
			
		||||
current_version = 2023.4.1
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -112,7 +112,7 @@ jobs:
 | 
			
		||||
      - name: Setup authentik env
 | 
			
		||||
        uses: ./.github/actions/setup
 | 
			
		||||
      - name: Create k8s Kind Cluster
 | 
			
		||||
        uses: helm/kind-action@v1.7.0
 | 
			
		||||
        uses: helm/kind-action@v1.5.0
 | 
			
		||||
      - name: run integration
 | 
			
		||||
        run: |
 | 
			
		||||
          poetry run coverage run manage.py test tests/integration
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -135,5 +135,4 @@ jobs:
 | 
			
		||||
          set -x
 | 
			
		||||
          export GOOS=${{ matrix.goos }}
 | 
			
		||||
          export GOARCH=${{ matrix.goarch }}
 | 
			
		||||
          export CGO_ENABLED=0
 | 
			
		||||
          go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
								
							@ -10,11 +10,6 @@ jobs:
 | 
			
		||||
    name: Delete old unused container images
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - id: generate_token
 | 
			
		||||
        uses: tibdex/github-app-token@v1
 | 
			
		||||
        with:
 | 
			
		||||
          app_id: ${{ secrets.GH_APP_ID }}
 | 
			
		||||
          private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
 | 
			
		||||
      - name: Delete 'dev' containers older than a week
 | 
			
		||||
        uses: snok/container-retention-policy@v2
 | 
			
		||||
        with:
 | 
			
		||||
@ -23,5 +18,5 @@ jobs:
 | 
			
		||||
          account-type: org
 | 
			
		||||
          org-name: goauthentik
 | 
			
		||||
          untagged-only: false
 | 
			
		||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
          skip-tags: gh-next,gh-main
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -123,7 +123,6 @@ jobs:
 | 
			
		||||
          set -x
 | 
			
		||||
          export GOOS=${{ matrix.goos }}
 | 
			
		||||
          export GOARCH=${{ matrix.goarch }}
 | 
			
		||||
          export CGO_ENABLED=0
 | 
			
		||||
          go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
 | 
			
		||||
      - name: Upload binaries to release
 | 
			
		||||
        uses: svenstaro/upload-release-action@v2
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							@ -22,23 +22,18 @@ jobs:
 | 
			
		||||
          docker-compose up --no-start
 | 
			
		||||
          docker-compose start postgresql redis
 | 
			
		||||
          docker-compose run -u root server test-all
 | 
			
		||||
      - id: generate_token
 | 
			
		||||
        uses: tibdex/github-app-token@v1
 | 
			
		||||
        with:
 | 
			
		||||
          app_id: ${{ secrets.GH_APP_ID }}
 | 
			
		||||
          private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
 | 
			
		||||
      - name: Extract version number
 | 
			
		||||
        id: get_version
 | 
			
		||||
        uses: actions/github-script@v6
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
            return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
 | 
			
		||||
      - name: Create Release
 | 
			
		||||
        id: create_release
 | 
			
		||||
        uses: actions/create-release@v1.1.4
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
        with:
 | 
			
		||||
          tag_name: ${{ github.ref }}
 | 
			
		||||
          release_name: Release ${{ steps.get_version.outputs.result }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							@ -15,14 +15,9 @@ jobs:
 | 
			
		||||
  compile:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - id: generate_token
 | 
			
		||||
        uses: tibdex/github-app-token@v1
 | 
			
		||||
        with:
 | 
			
		||||
          app_id: ${{ secrets.GH_APP_ID }}
 | 
			
		||||
          private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
      - name: Setup authentik env
 | 
			
		||||
        uses: ./.github/actions/setup
 | 
			
		||||
      - name: run compile
 | 
			
		||||
@ -31,7 +26,7 @@ jobs:
 | 
			
		||||
        uses: peter-evans/create-pull-request@v5
 | 
			
		||||
        id: cpr
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
          branch: compile-backend-translation
 | 
			
		||||
          commit-message: "core: compile backend translations"
 | 
			
		||||
          title: "core: compile backend translations"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -9,14 +9,9 @@ jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - id: generate_token
 | 
			
		||||
        uses: tibdex/github-app-token@v1
 | 
			
		||||
        with:
 | 
			
		||||
          app_id: ${{ secrets.GH_APP_ID }}
 | 
			
		||||
          private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
      - uses: actions/setup-node@v3.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: "20"
 | 
			
		||||
@ -38,7 +33,7 @@ jobs:
 | 
			
		||||
      - uses: peter-evans/create-pull-request@v5
 | 
			
		||||
        id: cpr
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
          branch: update-web-api-client
 | 
			
		||||
          commit-message: "web: bump API Client version"
 | 
			
		||||
          title: "web: bump API Client version"
 | 
			
		||||
@ -49,6 +44,6 @@ jobs:
 | 
			
		||||
          author: authentik bot <github-bot@goauthentik.io>
 | 
			
		||||
      - uses: peter-evans/enable-pull-request-automerge@v3
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ steps.generate_token.outputs.token }}
 | 
			
		||||
          token: ${{ secrets.BOT_GITHUB_TOKEN }}
 | 
			
		||||
          pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
 | 
			
		||||
          merge-method: squash
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							@ -1,11 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
    "recommendations": [
 | 
			
		||||
        "EditorConfig.EditorConfig",
 | 
			
		||||
        "bashmish.es6-string-css",
 | 
			
		||||
        "bpruitt-goddard.mermaid-markdown-syntax-highlighting",
 | 
			
		||||
        "dbaeumer.vscode-eslint",
 | 
			
		||||
        "EditorConfig.EditorConfig",
 | 
			
		||||
        "esbenp.prettier-vscode",
 | 
			
		||||
        "github.vscode-github-actions",
 | 
			
		||||
        "golang.go",
 | 
			
		||||
        "Gruntfuggly.todo-tree",
 | 
			
		||||
        "mechatroner.rainbow-csv",
 | 
			
		||||
@ -16,6 +15,6 @@
 | 
			
		||||
        "ms-python.vscode-pylance",
 | 
			
		||||
        "redhat.vscode-yaml",
 | 
			
		||||
        "Tobermory.es6-string-html",
 | 
			
		||||
        "unifiedjs.vscode-mdx",
 | 
			
		||||
        "unifiedjs.vscode-mdx"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@ -48,10 +48,5 @@
 | 
			
		||||
            "ignoreCase": false
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "go.testFlags": [
 | 
			
		||||
        "-count=1"
 | 
			
		||||
    ],
 | 
			
		||||
    "github-actions.workflows.pinned.workflows": [
 | 
			
		||||
        ".github/workflows/ci-main.yml"
 | 
			
		||||
    ]
 | 
			
		||||
    "go.testFlags": ["-count=1"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ COPY ./SECURITY.md /work/
 | 
			
		||||
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
WORKDIR /work/website
 | 
			
		||||
RUN npm ci --include=dev && npm run build-docs-only
 | 
			
		||||
RUN npm ci && npm run build-docs-only
 | 
			
		||||
 | 
			
		||||
# Stage 2: Build webui
 | 
			
		||||
FROM --platform=${BUILDPLATFORM} docker.io/node:20 as web-builder
 | 
			
		||||
@ -17,7 +17,7 @@ COPY ./website /work/website/
 | 
			
		||||
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
WORKDIR /work/web
 | 
			
		||||
RUN npm ci --include=dev && npm run build
 | 
			
		||||
RUN npm ci && npm run build
 | 
			
		||||
 | 
			
		||||
# Stage 3: Poetry to requirements.txt export
 | 
			
		||||
FROM docker.io/python:3.11.3-slim-bullseye AS poetry-locker
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,8 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
 | 
			
		||||
 | 
			
		||||
| Version   | Supported          |
 | 
			
		||||
| --------- | ------------------ |
 | 
			
		||||
| 2023.4.x  | :white_check_mark: |
 | 
			
		||||
| 2023.5.x  | :white_check_mark: |
 | 
			
		||||
| 2023.2.x  | :white_check_mark: |
 | 
			
		||||
| 2023.3.x  | :white_check_mark: |
 | 
			
		||||
 | 
			
		||||
## Reporting a Vulnerability
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
from os import environ
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
__version__ = "2023.5.6"
 | 
			
		||||
__version__ = "2023.4.1"
 | 
			
		||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
"""authentik administration overview"""
 | 
			
		||||
import os
 | 
			
		||||
import platform
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from sys import version as python_version
 | 
			
		||||
@ -33,6 +34,7 @@ class RuntimeDict(TypedDict):
 | 
			
		||||
class SystemSerializer(PassiveSerializer):
 | 
			
		||||
    """Get system information."""
 | 
			
		||||
 | 
			
		||||
    env = SerializerMethodField()
 | 
			
		||||
    http_headers = SerializerMethodField()
 | 
			
		||||
    http_host = SerializerMethodField()
 | 
			
		||||
    http_is_secure = SerializerMethodField()
 | 
			
		||||
@ -41,6 +43,10 @@ class SystemSerializer(PassiveSerializer):
 | 
			
		||||
    server_time = SerializerMethodField()
 | 
			
		||||
    embedded_outpost_host = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_env(self, request: Request) -> dict[str, str]:
 | 
			
		||||
        """Get Environment"""
 | 
			
		||||
        return os.environ.copy()
 | 
			
		||||
 | 
			
		||||
    def get_http_headers(self, request: Request) -> dict[str, str]:
 | 
			
		||||
        """Get HTTP Request headers"""
 | 
			
		||||
        headers = {}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
"""API Authentication"""
 | 
			
		||||
from hmac import compare_digest
 | 
			
		||||
from typing import Any, Optional
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
@ -79,7 +78,7 @@ def token_secret_key(value: str) -> Optional[User]:
 | 
			
		||||
    and return the service account for the managed outpost"""
 | 
			
		||||
    from authentik.outposts.apps import MANAGED_OUTPOST
 | 
			
		||||
 | 
			
		||||
    if not compare_digest(value, settings.SECRET_KEY):
 | 
			
		||||
    if value != settings.SECRET_KEY:
 | 
			
		||||
        return None
 | 
			
		||||
    outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
			
		||||
    if not outposts:
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
"""core Configs API"""
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from os import path
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import models
 | 
			
		||||
@ -63,7 +63,7 @@ class ConfigView(APIView):
 | 
			
		||||
        """Get all capabilities this server instance supports"""
 | 
			
		||||
        caps = []
 | 
			
		||||
        deb_test = settings.DEBUG or settings.TEST
 | 
			
		||||
        if Path(settings.MEDIA_ROOT).is_mount() or deb_test:
 | 
			
		||||
        if path.ismount(settings.MEDIA_ROOT) or deb_test:
 | 
			
		||||
            caps.append(Capabilities.CAN_SAVE_MEDIA)
 | 
			
		||||
        if GEOIP_READER.enabled:
 | 
			
		||||
            caps.append(Capabilities.CAN_GEO_IP)
 | 
			
		||||
 | 
			
		||||
@ -11,9 +11,8 @@ from rest_framework.serializers import ListSerializer, ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.api.decorators import permission_required
 | 
			
		||||
from authentik.blueprints.models import BlueprintInstance
 | 
			
		||||
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
 | 
			
		||||
from authentik.blueprints.v1.importer import Importer
 | 
			
		||||
from authentik.blueprints.v1.oci import OCI_PREFIX
 | 
			
		||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
 | 
			
		||||
from authentik.core.api.used_by import UsedByMixin
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
@ -36,12 +35,11 @@ class BlueprintInstanceSerializer(ModelSerializer):
 | 
			
		||||
    """Info about a single blueprint instance file"""
 | 
			
		||||
 | 
			
		||||
    def validate_path(self, path: str) -> str:
 | 
			
		||||
        """Ensure the path (if set) specified is retrievable"""
 | 
			
		||||
        if path == "" or path.startswith(OCI_PREFIX):
 | 
			
		||||
            return path
 | 
			
		||||
        files: list[dict] = blueprints_find_dict.delay().get()
 | 
			
		||||
        if path not in [file["path"] for file in files]:
 | 
			
		||||
            raise ValidationError(_("Blueprint file does not exist"))
 | 
			
		||||
        """Ensure the path specified is retrievable"""
 | 
			
		||||
        try:
 | 
			
		||||
            BlueprintInstance(path=path).retrieve()
 | 
			
		||||
        except BlueprintRetrievalFailed as exc:
 | 
			
		||||
            raise ValidationError(exc) from exc
 | 
			
		||||
        return path
 | 
			
		||||
 | 
			
		||||
    def validate_content(self, content: str) -> str:
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ from rest_framework.serializers import Serializer
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, is_model_allowed
 | 
			
		||||
from authentik.blueprints.v1.meta.registry import BaseMetaModel, registry
 | 
			
		||||
from authentik.blueprints.v1.meta.registry import registry
 | 
			
		||||
from authentik.lib.models import SerializerModel
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
@ -74,9 +74,6 @@ class Command(BaseCommand):
 | 
			
		||||
    def build(self):
 | 
			
		||||
        """Build all models into the schema"""
 | 
			
		||||
        for model in registry.get_models():
 | 
			
		||||
            if issubclass(model, BaseMetaModel):
 | 
			
		||||
                serializer_class = model.serializer()
 | 
			
		||||
            else:
 | 
			
		||||
            if model._meta.abstract:
 | 
			
		||||
                continue
 | 
			
		||||
            if not is_model_allowed(model):
 | 
			
		||||
@ -84,8 +81,7 @@ class Command(BaseCommand):
 | 
			
		||||
            model_instance: Model = model()
 | 
			
		||||
            if not isinstance(model_instance, SerializerModel):
 | 
			
		||||
                continue
 | 
			
		||||
                serializer_class = model_instance.serializer
 | 
			
		||||
            serializer = serializer_class(
 | 
			
		||||
            serializer = model_instance.serializer(
 | 
			
		||||
                context={
 | 
			
		||||
                    SERIALIZER_CONTEXT_BLUEPRINT: False,
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
 | 
			
		||||
            enabled=True,
 | 
			
		||||
            managed_models=[],
 | 
			
		||||
            last_applied_hash="",
 | 
			
		||||
            metadata=metadata or {},
 | 
			
		||||
            metadata=metadata,
 | 
			
		||||
        )
 | 
			
		||||
        instance.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.blueprints.v1.oci import OCI_PREFIX, BlueprintOCIClient, OCIException
 | 
			
		||||
from authentik.blueprints.v1.oci import BlueprintOCIClient, OCIException
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
 | 
			
		||||
from authentik.lib.sentry import SentryIgnoredException
 | 
			
		||||
@ -72,7 +72,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
 | 
			
		||||
 | 
			
		||||
    def retrieve_oci(self) -> str:
 | 
			
		||||
        """Get blueprint from an OCI registry"""
 | 
			
		||||
        client = BlueprintOCIClient(self.path.replace(OCI_PREFIX, "https://"))
 | 
			
		||||
        client = BlueprintOCIClient(self.path.replace("oci://", "https://"))
 | 
			
		||||
        try:
 | 
			
		||||
            manifests = client.fetch_manifests()
 | 
			
		||||
            return client.fetch_blobs(manifests)
 | 
			
		||||
@ -82,10 +82,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
 | 
			
		||||
    def retrieve_file(self) -> str:
 | 
			
		||||
        """Get blueprint from path"""
 | 
			
		||||
        try:
 | 
			
		||||
            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")
 | 
			
		||||
            full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
 | 
			
		||||
            with full_path.open("r", encoding="utf-8") as _file:
 | 
			
		||||
                return _file.read()
 | 
			
		||||
        except (IOError, OSError) as exc:
 | 
			
		||||
@ -93,7 +90,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
 | 
			
		||||
 | 
			
		||||
    def retrieve(self) -> str:
 | 
			
		||||
        """Retrieve blueprint contents"""
 | 
			
		||||
        if self.path.startswith(OCI_PREFIX):
 | 
			
		||||
        if self.path.startswith("oci://"):
 | 
			
		||||
            return self.retrieve_oci()
 | 
			
		||||
        if self.path != "":
 | 
			
		||||
            return self.retrieve_file()
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,34 @@
 | 
			
		||||
"""authentik managed models tests"""
 | 
			
		||||
from typing import Callable, Type
 | 
			
		||||
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
from authentik.blueprints.v1.importer import is_model_allowed
 | 
			
		||||
from authentik.lib.models import SerializerModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestModels(TestCase):
 | 
			
		||||
    """Test Models"""
 | 
			
		||||
 | 
			
		||||
    def test_retrieve_file(self):
 | 
			
		||||
        """Test retrieve_file"""
 | 
			
		||||
        instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
 | 
			
		||||
        with self.assertRaises(BlueprintRetrievalFailed):
 | 
			
		||||
            instance.retrieve()
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
 | 
			
		||||
@ -32,29 +32,6 @@ class TestBlueprintOCI(TransactionTestCase):
 | 
			
		||||
                "foo",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_successful_port(self):
 | 
			
		||||
        """Successful retrieval with custom port"""
 | 
			
		||||
        with Mocker() as mocker:
 | 
			
		||||
            mocker.get(
 | 
			
		||||
                "https://ghcr.io:1234/v2/goauthentik/blueprints/test/manifests/latest",
 | 
			
		||||
                json={
 | 
			
		||||
                    "layers": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "mediaType": OCI_MEDIA_TYPE,
 | 
			
		||||
                            "digest": "foo",
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            mocker.get("https://ghcr.io:1234/v2/goauthentik/blueprints/test/blobs/foo", text="foo")
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                BlueprintInstance(
 | 
			
		||||
                    path="oci://ghcr.io:1234/goauthentik/blueprints/test:latest"
 | 
			
		||||
                ).retrieve(),
 | 
			
		||||
                "foo",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_manifests_error(self):
 | 
			
		||||
        """Test manifests request erroring"""
 | 
			
		||||
        with Mocker() as mocker:
 | 
			
		||||
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
"""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))
 | 
			
		||||
@ -44,14 +44,6 @@ class TestBlueprintsV1API(APITestCase):
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_api_oci(self):
 | 
			
		||||
        """Test validation with OCI path"""
 | 
			
		||||
        res = self.client.post(
 | 
			
		||||
            reverse("authentik_api:blueprintinstance-list"),
 | 
			
		||||
            data={"name": "foo", "path": "oci://foo/bar"},
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(res.status_code, 201)
 | 
			
		||||
 | 
			
		||||
    def test_api_blank(self):
 | 
			
		||||
        """Test blank"""
 | 
			
		||||
        res = self.client.post(
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,6 @@ from authentik.lib.sentry import SentryIgnoredException
 | 
			
		||||
from authentik.lib.utils.http import authentik_user_agent
 | 
			
		||||
 | 
			
		||||
OCI_MEDIA_TYPE = "application/vnd.goauthentik.blueprint.v1+yaml"
 | 
			
		||||
OCI_PREFIX = "oci://"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OCIException(SentryIgnoredException):
 | 
			
		||||
@ -40,16 +39,11 @@ class BlueprintOCIClient:
 | 
			
		||||
        self.logger = get_logger().bind(url=self.sanitized_url)
 | 
			
		||||
 | 
			
		||||
        self.ref = "latest"
 | 
			
		||||
        # Remove the leading slash of the path to convert it to an image name
 | 
			
		||||
        path = self.url.path[1:]
 | 
			
		||||
        if ":" in path:
 | 
			
		||||
            # if there's a colon in the path, use everything after it as a ref
 | 
			
		||||
        if ":" in self.url.path:
 | 
			
		||||
            path, _, self.ref = path.partition(":")
 | 
			
		||||
        base_url = f"https://{self.url.hostname}"
 | 
			
		||||
        if self.url.port:
 | 
			
		||||
            base_url += f":{self.url.port}"
 | 
			
		||||
        self.client = NewClient(
 | 
			
		||||
            base_url,
 | 
			
		||||
            f"https://{self.url.hostname}",
 | 
			
		||||
            WithUserAgent(authentik_user_agent()),
 | 
			
		||||
            WithUsernamePassword(self.url.username, self.url.password),
 | 
			
		||||
            WithDefaultName(path),
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,6 @@ from authentik.blueprints.models import (
 | 
			
		||||
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError
 | 
			
		||||
from authentik.blueprints.v1.importer import Importer
 | 
			
		||||
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
 | 
			
		||||
from authentik.blueprints.v1.oci import OCI_PREFIX
 | 
			
		||||
from authentik.events.monitored_tasks import (
 | 
			
		||||
    MonitoredTask,
 | 
			
		||||
    TaskResult,
 | 
			
		||||
@ -229,7 +228,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
 | 
			
		||||
def clear_failed_blueprints():
 | 
			
		||||
    """Remove blueprints which couldn't be fetched"""
 | 
			
		||||
    # Exclude OCI blueprints as those might be temporarily unavailable
 | 
			
		||||
    for blueprint in BlueprintInstance.objects.exclude(path__startswith=OCI_PREFIX):
 | 
			
		||||
    for blueprint in BlueprintInstance.objects.exclude(path__startswith="oci://"):
 | 
			
		||||
        try:
 | 
			
		||||
            blueprint.retrieve()
 | 
			
		||||
        except BlueprintRetrievalFailed:
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
"""Provider API Views"""
 | 
			
		||||
from django.db.models import QuerySet
 | 
			
		||||
from django.db.models.query import Q
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_filters.filters import BooleanFilter
 | 
			
		||||
from django_filters.filterset import FilterSet
 | 
			
		||||
@ -58,22 +56,17 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProviderFilter(FilterSet):
 | 
			
		||||
    """Filter for providers"""
 | 
			
		||||
    """Filter for groups"""
 | 
			
		||||
 | 
			
		||||
    application__isnull = BooleanFilter(method="filter_application__isnull")
 | 
			
		||||
    application__isnull = BooleanFilter(
 | 
			
		||||
        field_name="application",
 | 
			
		||||
        lookup_expr="isnull",
 | 
			
		||||
    )
 | 
			
		||||
    backchannel_only = BooleanFilter(
 | 
			
		||||
        method="filter_backchannel_only",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_application__isnull(self, queryset: QuerySet, name, value):
 | 
			
		||||
        """Only return providers that are neither assigned to application,
 | 
			
		||||
        both as provider or application provider"""
 | 
			
		||||
        return queryset.filter(
 | 
			
		||||
            Q(backchannel_application__isnull=value, is_backchannel=True)
 | 
			
		||||
            | Q(application__isnull=value)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def filter_backchannel_only(self, queryset: QuerySet, name, value):
 | 
			
		||||
    def filter_backchannel_only(self, queryset, name, value):
 | 
			
		||||
        """Only return backchannel providers"""
 | 
			
		||||
        return queryset.filter(is_backchannel=value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -67,12 +67,11 @@ from authentik.core.models import (
 | 
			
		||||
    TokenIntents,
 | 
			
		||||
    User,
 | 
			
		||||
)
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.events.models import EventAction
 | 
			
		||||
from authentik.flows.exceptions import FlowNonApplicableException
 | 
			
		||||
from authentik.flows.models import FlowToken
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
 | 
			
		||||
from authentik.flows.views.executor import QS_KEY_TOKEN
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.stages.email.models import EmailStage
 | 
			
		||||
from authentik.stages.email.tasks import send_mails
 | 
			
		||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
			
		||||
@ -107,7 +106,7 @@ class UserSerializer(ModelSerializer):
 | 
			
		||||
    avatar = CharField(read_only=True)
 | 
			
		||||
    attributes = JSONField(validators=[is_dict], required=False)
 | 
			
		||||
    groups = PrimaryKeyRelatedField(
 | 
			
		||||
        allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all(), default=list
 | 
			
		||||
        allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all()
 | 
			
		||||
    )
 | 
			
		||||
    groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
 | 
			
		||||
    uid = CharField(read_only=True)
 | 
			
		||||
@ -544,58 +543,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
        send_mails(email_stage, message)
 | 
			
		||||
        return Response(status=204)
 | 
			
		||||
 | 
			
		||||
    @permission_required("authentik_core.impersonate")
 | 
			
		||||
    @extend_schema(
 | 
			
		||||
        request=OpenApiTypes.NONE,
 | 
			
		||||
        responses={
 | 
			
		||||
            "204": OpenApiResponse(description="Successfully started impersonation"),
 | 
			
		||||
            "401": OpenApiResponse(description="Access denied"),
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    @action(detail=True, methods=["POST"])
 | 
			
		||||
    def impersonate(self, request: Request, pk: int) -> Response:
 | 
			
		||||
        """Impersonate a user"""
 | 
			
		||||
        if not CONFIG.y_bool("impersonation"):
 | 
			
		||||
            LOGGER.debug("User attempted to impersonate", user=request.user)
 | 
			
		||||
            return Response(status=401)
 | 
			
		||||
        if not request.user.has_perm("impersonate"):
 | 
			
		||||
            LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
 | 
			
		||||
            return Response(status=401)
 | 
			
		||||
 | 
			
		||||
        user_to_be = self.get_object()
 | 
			
		||||
 | 
			
		||||
        request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
 | 
			
		||||
        request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
 | 
			
		||||
 | 
			
		||||
        Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
 | 
			
		||||
 | 
			
		||||
        return Response(status=201)
 | 
			
		||||
 | 
			
		||||
    @extend_schema(
 | 
			
		||||
        request=OpenApiTypes.NONE,
 | 
			
		||||
        responses={
 | 
			
		||||
            "204": OpenApiResponse(description="Successfully started impersonation"),
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    @action(detail=False, methods=["GET"])
 | 
			
		||||
    def impersonate_end(self, request: Request) -> Response:
 | 
			
		||||
        """End Impersonation a user"""
 | 
			
		||||
        if (
 | 
			
		||||
            SESSION_KEY_IMPERSONATE_USER not in request.session
 | 
			
		||||
            or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
 | 
			
		||||
        ):
 | 
			
		||||
            LOGGER.debug("Can't end impersonation", user=request.user)
 | 
			
		||||
            return Response(status=204)
 | 
			
		||||
 | 
			
		||||
        original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
 | 
			
		||||
 | 
			
		||||
        del request.session[SESSION_KEY_IMPERSONATE_USER]
 | 
			
		||||
        del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
 | 
			
		||||
 | 
			
		||||
        Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
 | 
			
		||||
 | 
			
		||||
        return Response(status=204)
 | 
			
		||||
 | 
			
		||||
    def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
 | 
			
		||||
        """Custom filter_queryset method which ignores guardian, but still supports sorting"""
 | 
			
		||||
        for backend in list(self.filter_backends):
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSI
 | 
			
		||||
from authentik.lib.utils.urls import redirect_with_qs
 | 
			
		||||
from authentik.lib.views import bad_request_message
 | 
			
		||||
from authentik.policies.denied import AccessDeniedResponse
 | 
			
		||||
from authentik.policies.utils import delete_none_values
 | 
			
		||||
from authentik.policies.utils import delete_none_keys
 | 
			
		||||
from authentik.stages.password import BACKEND_INBUILT
 | 
			
		||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
			
		||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
 | 
			
		||||
@ -329,7 +329,7 @@ class SourceFlowManager:
 | 
			
		||||
                )
 | 
			
		||||
            ],
 | 
			
		||||
            **{
 | 
			
		||||
                PLAN_CONTEXT_PROMPT: delete_none_values(self.enroll_info),
 | 
			
		||||
                PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
 | 
			
		||||
                PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,8 @@
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
 | 
			
		||||
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
 | 
			
		||||
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
 | 
			
		||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
 | 
			
		||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
 | 
			
		||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
 | 
			
		||||
{% include "base/header_js.html" %}
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,14 @@
 | 
			
		||||
"""impersonation tests"""
 | 
			
		||||
from json import loads
 | 
			
		||||
 | 
			
		||||
from django.test.testcases import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from rest_framework.test import APITestCase
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.core.tests.utils import create_test_admin_user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestImpersonation(APITestCase):
 | 
			
		||||
class TestImpersonation(TestCase):
 | 
			
		||||
    """impersonation tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
@ -23,10 +23,10 @@ class TestImpersonation(APITestCase):
 | 
			
		||||
        self.other_user.save()
 | 
			
		||||
        self.client.force_login(self.user)
 | 
			
		||||
 | 
			
		||||
        self.client.post(
 | 
			
		||||
        self.client.get(
 | 
			
		||||
            reverse(
 | 
			
		||||
                "authentik_api:user-impersonate",
 | 
			
		||||
                kwargs={"pk": self.other_user.pk},
 | 
			
		||||
                "authentik_core:impersonate-init",
 | 
			
		||||
                kwargs={"user_id": self.other_user.pk},
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ class TestImpersonation(APITestCase):
 | 
			
		||||
        self.assertEqual(response_body["user"]["username"], self.other_user.username)
 | 
			
		||||
        self.assertEqual(response_body["original"]["username"], self.user.username)
 | 
			
		||||
 | 
			
		||||
        self.client.get(reverse("authentik_api:user-impersonate-end"))
 | 
			
		||||
        self.client.get(reverse("authentik_core:impersonate-end"))
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:user-me"))
 | 
			
		||||
        response_body = loads(response.content.decode())
 | 
			
		||||
@ -46,7 +46,9 @@ class TestImpersonation(APITestCase):
 | 
			
		||||
        """test impersonation without permissions"""
 | 
			
		||||
        self.client.force_login(self.other_user)
 | 
			
		||||
 | 
			
		||||
        self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
 | 
			
		||||
        self.client.get(
 | 
			
		||||
            reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:user-me"))
 | 
			
		||||
        response_body = loads(response.content.decode())
 | 
			
		||||
@ -56,5 +58,5 @@ class TestImpersonation(APITestCase):
 | 
			
		||||
        """test un-impersonation without impersonating first"""
 | 
			
		||||
        self.client.force_login(self.other_user)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:user-impersonate-end"))
 | 
			
		||||
        self.assertEqual(response.status_code, 204)
 | 
			
		||||
        response = self.client.get(reverse("authentik_core:impersonate-end"))
 | 
			
		||||
        self.assertRedirects(response, reverse("authentik_core:if-user"))
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ from authentik.core.api.providers import ProviderViewSet
 | 
			
		||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
 | 
			
		||||
from authentik.core.api.tokens import TokenViewSet
 | 
			
		||||
from authentik.core.api.users import UserViewSet
 | 
			
		||||
from authentik.core.views import apps
 | 
			
		||||
from authentik.core.views import apps, impersonate
 | 
			
		||||
from authentik.core.views.debug import AccessDeniedView
 | 
			
		||||
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
 | 
			
		||||
from authentik.core.views.session import EndSessionView
 | 
			
		||||
@ -38,6 +38,17 @@ urlpatterns = [
 | 
			
		||||
        apps.RedirectToAppLaunch.as_view(),
 | 
			
		||||
        name="application-launch",
 | 
			
		||||
    ),
 | 
			
		||||
    # Impersonation
 | 
			
		||||
    path(
 | 
			
		||||
        "-/impersonation/<int:user_id>/",
 | 
			
		||||
        impersonate.ImpersonateInitView.as_view(),
 | 
			
		||||
        name="impersonate-init",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "-/impersonation/end/",
 | 
			
		||||
        impersonate.ImpersonateEndView.as_view(),
 | 
			
		||||
        name="impersonate-end",
 | 
			
		||||
    ),
 | 
			
		||||
    # Interfaces
 | 
			
		||||
    path(
 | 
			
		||||
        "if/admin/",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										60
									
								
								authentik/core/views/impersonate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								authentik/core/views/impersonate.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
"""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")
 | 
			
		||||
@ -7,6 +7,7 @@ from smtplib import SMTPException
 | 
			
		||||
from typing import TYPE_CHECKING, Optional
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Count, ExpressionWrapper, F
 | 
			
		||||
from django.db.models.fields import DurationField
 | 
			
		||||
@ -206,7 +207,9 @@ class Event(SerializerModel, ExpiringModel):
 | 
			
		||||
        self.user = get_user(user)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def from_http(self, request: HttpRequest, user: Optional[User] = None) -> "Event":
 | 
			
		||||
    def from_http(
 | 
			
		||||
        self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
 | 
			
		||||
    ) -> "Event":
 | 
			
		||||
        """Add data from a Django-HttpRequest, allowing the creation of
 | 
			
		||||
        Events independently from requests.
 | 
			
		||||
        `user` arguments optionally overrides user from requests."""
 | 
			
		||||
 | 
			
		||||
@ -87,9 +87,9 @@ class TaskInfo:
 | 
			
		||||
        except TypeError:
 | 
			
		||||
            duration = 0
 | 
			
		||||
        GAUGE_TASKS.labels(
 | 
			
		||||
            task_name=self.task_name.split(":")[0],
 | 
			
		||||
            task_name=self.task_name,
 | 
			
		||||
            task_uid=self.result.uid or "",
 | 
			
		||||
            status=self.result.status.value,
 | 
			
		||||
            status=self.result.status,
 | 
			
		||||
        ).set(duration)
 | 
			
		||||
 | 
			
		||||
    def save(self, timeout_hours=6):
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@
 | 
			
		||||
import re
 | 
			
		||||
from copy import copy
 | 
			
		||||
from dataclasses import asdict, is_dataclass
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from types import GeneratorType
 | 
			
		||||
from typing import Any, Optional
 | 
			
		||||
@ -127,8 +126,6 @@ def sanitize_item(value: Any) -> Any:
 | 
			
		||||
        return str(value)
 | 
			
		||||
    if isinstance(value, YAMLTag):
 | 
			
		||||
        return str(value)
 | 
			
		||||
    if isinstance(value, Enum):
 | 
			
		||||
        return value.value
 | 
			
		||||
    if isinstance(value, type):
 | 
			
		||||
        return {
 | 
			
		||||
            "type": value.__name__,
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,7 @@ class DiagramElement:
 | 
			
		||||
    style: list[str] = field(default_factory=lambda: ["[", "]"])
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        description = self.description.replace('"', "#quot;")
 | 
			
		||||
        element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
 | 
			
		||||
        element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
 | 
			
		||||
        if self.action is not None:
 | 
			
		||||
            if self.action != "":
 | 
			
		||||
                element = f"--{self.action}--> {element}"
 | 
			
		||||
 | 
			
		||||
@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
 | 
			
		||||
        for field, errors in response.errors.items():
 | 
			
		||||
            for error in errors:
 | 
			
		||||
                full_errors.setdefault(field, [])
 | 
			
		||||
                field_error = {
 | 
			
		||||
                full_errors[field].append(
 | 
			
		||||
                    {
 | 
			
		||||
                        "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
 | 
			
		||||
        if not challenge_response.is_valid():
 | 
			
		||||
            self.logger.error(
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ from contextlib import contextmanager
 | 
			
		||||
from glob import glob
 | 
			
		||||
from json import dumps, loads
 | 
			
		||||
from json.decoder import JSONDecodeError
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from sys import argv, stderr
 | 
			
		||||
from time import time
 | 
			
		||||
from typing import Any
 | 
			
		||||
@ -43,25 +42,22 @@ class ConfigLoader:
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.__config = {}
 | 
			
		||||
        base_dir = Path(__file__).parent.joinpath(Path("../..")).resolve()
 | 
			
		||||
        for _path in SEARCH_PATHS:
 | 
			
		||||
            path = Path(_path)
 | 
			
		||||
        base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../.."))
 | 
			
		||||
        for path in SEARCH_PATHS:
 | 
			
		||||
            # Check if path is relative, and if so join with base_dir
 | 
			
		||||
            if not path.is_absolute():
 | 
			
		||||
                path = base_dir / path
 | 
			
		||||
            if path.is_file() and path.exists():
 | 
			
		||||
            if not os.path.isabs(path):
 | 
			
		||||
                path = os.path.join(base_dir, path)
 | 
			
		||||
            if os.path.isfile(path) and os.path.exists(path):
 | 
			
		||||
                # Path is an existing file, so we just read it and update our config with it
 | 
			
		||||
                self.update_from_file(path)
 | 
			
		||||
            elif path.is_dir() and path.exists():
 | 
			
		||||
            elif os.path.isdir(path) and os.path.exists(path):
 | 
			
		||||
                # Path is an existing dir, so we try to read the env config from it
 | 
			
		||||
                env_paths = [
 | 
			
		||||
                    path / Path(ENVIRONMENT + ".yml"),
 | 
			
		||||
                    path / Path(ENVIRONMENT + ".env.yml"),
 | 
			
		||||
                    path / Path(ENVIRONMENT + ".yaml"),
 | 
			
		||||
                    path / Path(ENVIRONMENT + ".env.yaml"),
 | 
			
		||||
                    os.path.join(path, ENVIRONMENT + ".yml"),
 | 
			
		||||
                    os.path.join(path, ENVIRONMENT + ".env.yml"),
 | 
			
		||||
                ]
 | 
			
		||||
                for env_file in env_paths:
 | 
			
		||||
                    if env_file.is_file() and env_file.exists():
 | 
			
		||||
                    if os.path.isfile(env_file) and os.path.exists(env_file):
 | 
			
		||||
                        # Update config with env file
 | 
			
		||||
                        self.update_from_file(env_file)
 | 
			
		||||
        self.update_from_env()
 | 
			
		||||
@ -103,13 +99,13 @@ class ConfigLoader:
 | 
			
		||||
                value = url.query
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    def update_from_file(self, path: Path):
 | 
			
		||||
    def update_from_file(self, path: str):
 | 
			
		||||
        """Update config from file contents"""
 | 
			
		||||
        try:
 | 
			
		||||
            with open(path, encoding="utf8") as file:
 | 
			
		||||
                try:
 | 
			
		||||
                    self.update(self.__config, yaml.safe_load(file))
 | 
			
		||||
                    self.log("debug", "Loaded config", file=str(path))
 | 
			
		||||
                    self.log("debug", "Loaded config", file=path)
 | 
			
		||||
                    self.loaded_file.append(path)
 | 
			
		||||
                except yaml.YAMLError as exc:
 | 
			
		||||
                    raise ImproperlyConfigured from exc
 | 
			
		||||
 | 
			
		||||
@ -5,25 +5,18 @@ postgresql:
 | 
			
		||||
  name: authentik
 | 
			
		||||
  user: authentik
 | 
			
		||||
  port: 5432
 | 
			
		||||
  password: "env://POSTGRES_PASSWORD"
 | 
			
		||||
  password: 'env://POSTGRES_PASSWORD'
 | 
			
		||||
  use_pgbouncer: false
 | 
			
		||||
 | 
			
		||||
listen:
 | 
			
		||||
  listen_http: 0.0.0.0:9000
 | 
			
		||||
  listen_https: 0.0.0.0:9443
 | 
			
		||||
  listen_metrics: 0.0.0.0:9300
 | 
			
		||||
  trusted_proxy_cidrs:
 | 
			
		||||
    - 127.0.0.0/8
 | 
			
		||||
    - 10.0.0.0/8
 | 
			
		||||
    - 172.16.0.0/12
 | 
			
		||||
    - 192.168.0.0/16
 | 
			
		||||
    - fe80::/10
 | 
			
		||||
    - ::1/128
 | 
			
		||||
 | 
			
		||||
redis:
 | 
			
		||||
  host: localhost
 | 
			
		||||
  port: 6379
 | 
			
		||||
  password: ""
 | 
			
		||||
  password: ''
 | 
			
		||||
  tls: false
 | 
			
		||||
  tls_reqs: "none"
 | 
			
		||||
  db: 0
 | 
			
		||||
 | 
			
		||||
@ -140,21 +140,19 @@ class BaseEvaluator:
 | 
			
		||||
    def expr_event_create(self, action: str, **kwargs):
 | 
			
		||||
        """Create event with supplied data and try to extract as much relevant data
 | 
			
		||||
        from the context"""
 | 
			
		||||
        context = self._context.copy()
 | 
			
		||||
        # If the result was a complex variable, we don't want to re-use it
 | 
			
		||||
        context.pop("result", None)
 | 
			
		||||
        context.pop("handler", None)
 | 
			
		||||
        event_kwargs = context
 | 
			
		||||
        event_kwargs.update(kwargs)
 | 
			
		||||
        self._context.pop("result", None)
 | 
			
		||||
        self._context.pop("handler", None)
 | 
			
		||||
        kwargs["context"] = self._context
 | 
			
		||||
        event = Event.new(
 | 
			
		||||
            action,
 | 
			
		||||
            app=self._filename,
 | 
			
		||||
            **event_kwargs,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        if "request" in context and isinstance(context["request"], PolicyRequest):
 | 
			
		||||
            policy_request: PolicyRequest = context["request"]
 | 
			
		||||
        if "request" in self._context and isinstance(self._context["request"], PolicyRequest):
 | 
			
		||||
            policy_request: PolicyRequest = self._context["request"]
 | 
			
		||||
            if policy_request.http_request:
 | 
			
		||||
                event.from_http(policy_request.http_request)
 | 
			
		||||
                event.from_http(policy_request)
 | 
			
		||||
                return
 | 
			
		||||
        event.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,15 +19,7 @@ def fallback_names(app: str, model: str, field: str):
 | 
			
		||||
            if value not in seen_names:
 | 
			
		||||
                seen_names.append(value)
 | 
			
		||||
                continue
 | 
			
		||||
            separator = "_"
 | 
			
		||||
            suffix_index = 2
 | 
			
		||||
            while (
 | 
			
		||||
                klass.objects.using(db_alias)
 | 
			
		||||
                .filter(**{field: f"{value}{separator}{suffix_index}"})
 | 
			
		||||
                .exists()
 | 
			
		||||
            ):
 | 
			
		||||
                suffix_index += 1
 | 
			
		||||
            new_value = f"{value}{separator}{suffix_index}"
 | 
			
		||||
            new_value = value + "_2"
 | 
			
		||||
            setattr(obj, field, new_value)
 | 
			
		||||
            obj.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,41 +2,28 @@
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.core.tests.utils import create_test_admin_user
 | 
			
		||||
from authentik.events.models import Event
 | 
			
		||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestEvaluator(TestCase):
 | 
			
		||||
    """Test Evaluator base functions"""
 | 
			
		||||
 | 
			
		||||
    def test_expr_regex_match(self):
 | 
			
		||||
    def test_regex_match(self):
 | 
			
		||||
        """Test expr_regex_match"""
 | 
			
		||||
        self.assertFalse(BaseEvaluator.expr_regex_match("foo", "bar"))
 | 
			
		||||
        self.assertTrue(BaseEvaluator.expr_regex_match("foo", "foo"))
 | 
			
		||||
 | 
			
		||||
    def test_expr_regex_replace(self):
 | 
			
		||||
    def test_regex_replace(self):
 | 
			
		||||
        """Test expr_regex_replace"""
 | 
			
		||||
        self.assertEqual(BaseEvaluator.expr_regex_replace("foo", "o", "a"), "faa")
 | 
			
		||||
 | 
			
		||||
    def test_expr_user_by(self):
 | 
			
		||||
    def test_user_by(self):
 | 
			
		||||
        """Test expr_user_by"""
 | 
			
		||||
        user = create_test_admin_user()
 | 
			
		||||
        self.assertIsNotNone(BaseEvaluator.expr_user_by(username=user.username))
 | 
			
		||||
        self.assertIsNone(BaseEvaluator.expr_user_by(username="bar"))
 | 
			
		||||
        self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar"))
 | 
			
		||||
 | 
			
		||||
    def test_expr_is_group_member(self):
 | 
			
		||||
    def test_is_group_member(self):
 | 
			
		||||
        """Test expr_is_group_member"""
 | 
			
		||||
        self.assertFalse(BaseEvaluator.expr_is_group_member(create_test_admin_user(), name="test"))
 | 
			
		||||
 | 
			
		||||
    def test_expr_event_create(self):
 | 
			
		||||
        """Test expr_event_create"""
 | 
			
		||||
        evaluator = BaseEvaluator(generate_id())
 | 
			
		||||
        evaluator._context = {
 | 
			
		||||
            "foo": "bar",
 | 
			
		||||
        }
 | 
			
		||||
        evaluator.evaluate("ak_create_event('foo', bar='baz')")
 | 
			
		||||
        event = Event.objects.filter(action="custom_foo").first()
 | 
			
		||||
        self.assertIsNotNone(event)
 | 
			
		||||
        self.assertEqual(event.context, {"bar": "baz", "foo": "bar"})
 | 
			
		||||
 | 
			
		||||
@ -16,12 +16,10 @@ LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
 | 
			
		||||
    """Attempt to get the client's IP by checking common HTTP Headers.
 | 
			
		||||
    Returns none if no IP Could be found
 | 
			
		||||
 | 
			
		||||
    No additional validation is done here as requests are expected to only arrive here
 | 
			
		||||
    via the go proxy, which deals with validating these headers for us"""
 | 
			
		||||
    Returns none if no IP Could be found"""
 | 
			
		||||
    headers = (
 | 
			
		||||
        "HTTP_X_FORWARDED_FOR",
 | 
			
		||||
        "HTTP_X_REAL_IP",
 | 
			
		||||
        "REMOTE_ADDR",
 | 
			
		||||
    )
 | 
			
		||||
    for _header in headers:
 | 
			
		||||
 | 
			
		||||
@ -42,15 +42,12 @@ from authentik.providers.ldap.controllers.docker import LDAPDockerController
 | 
			
		||||
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
 | 
			
		||||
from authentik.providers.proxy.controllers.docker import ProxyDockerController
 | 
			
		||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
 | 
			
		||||
from authentik.providers.radius.controllers.docker import RadiusDockerController
 | 
			
		||||
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
 | 
			
		||||
from authentik.root.celery import CELERY_APP
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=too-many-return-statements
 | 
			
		||||
def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]:
 | 
			
		||||
    """Get a controller for the outpost, when a service connection is defined"""
 | 
			
		||||
    if not outpost.service_connection:
 | 
			
		||||
@ -66,11 +63,6 @@ def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]:
 | 
			
		||||
            return LDAPDockerController
 | 
			
		||||
        if isinstance(service_connection, KubernetesServiceConnection):
 | 
			
		||||
            return LDAPKubernetesController
 | 
			
		||||
    if outpost.type == OutpostType.RADIUS:
 | 
			
		||||
        if isinstance(service_connection, DockerServiceConnection):
 | 
			
		||||
            return RadiusDockerController
 | 
			
		||||
        if isinstance(service_connection, KubernetesServiceConnection):
 | 
			
		||||
            return RadiusKubernetesController
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
 | 
			
		||||
        )
 | 
			
		||||
        binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
 | 
			
		||||
 | 
			
		||||
        http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
 | 
			
		||||
        http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
 | 
			
		||||
        http_request.user = self.user
 | 
			
		||||
        http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
 | 
			
		||||
        http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
 | 
			
		||||
 | 
			
		||||
        request = PolicyRequest(self.user)
 | 
			
		||||
        request.set_http_request(http_request)
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_none_values(dict_: dict[Any, Any]) -> dict[Any, Any]:
 | 
			
		||||
def delete_none_keys(dict_: dict[Any, Any]) -> dict[Any, Any]:
 | 
			
		||||
    """Remove any keys from `dict_` that are None."""
 | 
			
		||||
    new_dict = {}
 | 
			
		||||
    for key, value in dict_.items():
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,4 @@
 | 
			
		||||
"""LDAPProvider API Views"""
 | 
			
		||||
from django.db.models import QuerySet
 | 
			
		||||
from django.db.models.query import Q
 | 
			
		||||
from django_filters.filters import BooleanFilter
 | 
			
		||||
from django_filters.filterset import FilterSet
 | 
			
		||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
 | 
			
		||||
@ -33,21 +29,12 @@ class LDAPProviderSerializer(ProviderSerializer):
 | 
			
		||||
        extra_kwargs = ProviderSerializer.Meta.extra_kwargs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LDAPProviderFilter(FilterSet):
 | 
			
		||||
    """LDAP Provider filters"""
 | 
			
		||||
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    """LDAPProvider Viewset"""
 | 
			
		||||
 | 
			
		||||
    application__isnull = BooleanFilter(method="filter_application__isnull")
 | 
			
		||||
 | 
			
		||||
    def filter_application__isnull(self, queryset: QuerySet, name, value):
 | 
			
		||||
        """Only return providers that are neither assigned to application,
 | 
			
		||||
        both as provider or application provider"""
 | 
			
		||||
        return queryset.filter(
 | 
			
		||||
            Q(backchannel_application__isnull=value) | Q(application__isnull=value)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = LDAPProvider
 | 
			
		||||
        fields = {
 | 
			
		||||
    queryset = LDAPProvider.objects.all()
 | 
			
		||||
    serializer_class = LDAPProviderSerializer
 | 
			
		||||
    filterset_fields = {
 | 
			
		||||
        "application": ["isnull"],
 | 
			
		||||
        "name": ["iexact"],
 | 
			
		||||
        "authorization_flow__slug": ["iexact"],
 | 
			
		||||
@ -60,14 +47,6 @@ class LDAPProviderFilter(FilterSet):
 | 
			
		||||
        "uid_start_number": ["iexact"],
 | 
			
		||||
        "gid_start_number": ["iexact"],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    """LDAPProvider Viewset"""
 | 
			
		||||
 | 
			
		||||
    queryset = LDAPProvider.objects.all()
 | 
			
		||||
    serializer_class = LDAPProviderSerializer
 | 
			
		||||
    filterset_class = LDAPProviderFilter
 | 
			
		||||
    search_fields = ["name"]
 | 
			
		||||
    ordering = ["name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
"""RadiusProvider API Views"""
 | 
			
		||||
from rest_framework.fields import CharField, ListField
 | 
			
		||||
from rest_framework.fields import CharField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,6 @@ from authentik.providers.radius.models import RadiusProvider
 | 
			
		||||
class RadiusProviderSerializer(ProviderSerializer):
 | 
			
		||||
    """RadiusProvider Serializer"""
 | 
			
		||||
 | 
			
		||||
    outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = RadiusProvider
 | 
			
		||||
        fields = ProviderSerializer.Meta.fields + [
 | 
			
		||||
@ -20,7 +18,6 @@ class RadiusProviderSerializer(ProviderSerializer):
 | 
			
		||||
            # Shared secret is not a write-only field, as
 | 
			
		||||
            # an admin might have to view it
 | 
			
		||||
            "shared_secret",
 | 
			
		||||
            "outpost_set",
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = ProviderSerializer.Meta.extra_kwargs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,8 +24,8 @@ class SCIMProviderSerializer(ProviderSerializer):
 | 
			
		||||
            "property_mappings",
 | 
			
		||||
            "property_mappings_group",
 | 
			
		||||
            "component",
 | 
			
		||||
            "assigned_backchannel_application_slug",
 | 
			
		||||
            "assigned_backchannel_application_name",
 | 
			
		||||
            "assigned_application_slug",
 | 
			
		||||
            "assigned_application_name",
 | 
			
		||||
            "verbose_name",
 | 
			
		||||
            "verbose_name_plural",
 | 
			
		||||
            "meta_model_name",
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ class SCIMClient(Generic[T, SchemaType]):
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        except RequestException as exc:
 | 
			
		||||
            raise SCIMRequestException(message="Failed to send request") from exc
 | 
			
		||||
            raise SCIMRequestException(None) from exc
 | 
			
		||||
        self.logger.debug("scim request", path=path, method=method, **kwargs)
 | 
			
		||||
        if response.status_code >= 400:
 | 
			
		||||
            if response.status_code == 404:
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,10 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from pydantic import ValidationError
 | 
			
		||||
from pydanticscim.responses import SCIMError
 | 
			
		||||
from requests import Response
 | 
			
		||||
 | 
			
		||||
from authentik.lib.sentry import SentryIgnoredException
 | 
			
		||||
from authentik.providers.scim.clients.schema import SCIMError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StopSync(SentryIgnoredException):
 | 
			
		||||
@ -16,8 +16,7 @@ class StopSync(SentryIgnoredException):
 | 
			
		||||
        self.obj = obj
 | 
			
		||||
        self.mapping = mapping
 | 
			
		||||
 | 
			
		||||
    def detail(self) -> str:
 | 
			
		||||
        """Get human readable details of this error"""
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        msg = f"Error {str(self.exc)}, caused by {self.obj}"
 | 
			
		||||
 | 
			
		||||
        if self.mapping:
 | 
			
		||||
@ -29,22 +28,19 @@ class SCIMRequestException(SentryIgnoredException):
 | 
			
		||||
    """Exception raised when an SCIM request fails"""
 | 
			
		||||
 | 
			
		||||
    _response: Optional[Response]
 | 
			
		||||
    _message: Optional[str]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, response: Optional[Response] = None, message: Optional[str] = None) -> None:
 | 
			
		||||
    def __init__(self, response: Optional[Response] = None) -> None:
 | 
			
		||||
        self._response = response
 | 
			
		||||
        self._message = message
 | 
			
		||||
 | 
			
		||||
    def detail(self) -> str:
 | 
			
		||||
        """Get human readable details of this error"""
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        if not self._response:
 | 
			
		||||
            return self._message
 | 
			
		||||
            return super().__str__()
 | 
			
		||||
        try:
 | 
			
		||||
            error = SCIMError.parse_raw(self._response.text)
 | 
			
		||||
            return error.detail
 | 
			
		||||
        except ValidationError:
 | 
			
		||||
            pass
 | 
			
		||||
        return self._message
 | 
			
		||||
        return super().__str__()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResourceMissing(SCIMRequestException):
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
 | 
			
		||||
from authentik.core.models import Group
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.lib.utils.errors import exception_to_string
 | 
			
		||||
from authentik.policies.utils import delete_none_values
 | 
			
		||||
from authentik.policies.utils import delete_none_keys
 | 
			
		||||
from authentik.providers.scim.clients.base import SCIMClient
 | 
			
		||||
from authentik.providers.scim.clients.exceptions import (
 | 
			
		||||
    ResourceMissing,
 | 
			
		||||
@ -74,7 +74,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
 | 
			
		||||
        if not raw_scim_group:
 | 
			
		||||
            raise StopSync(ValueError("No group mappings configured"), obj)
 | 
			
		||||
        try:
 | 
			
		||||
            scim_group = SCIMGroupSchema.parse_obj(delete_none_values(raw_scim_group))
 | 
			
		||||
            scim_group = SCIMGroupSchema.parse_obj(delete_none_keys(raw_scim_group))
 | 
			
		||||
        except ValidationError as exc:
 | 
			
		||||
            raise StopSync(exc, obj) from exc
 | 
			
		||||
        if not scim_group.externalId:
 | 
			
		||||
@ -130,8 +130,10 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
 | 
			
		||||
                scim_group.id,
 | 
			
		||||
                PatchOperation(
 | 
			
		||||
                    op=PatchOp.replace,
 | 
			
		||||
                    path="displayName",
 | 
			
		||||
                    value=scim_group.displayName,
 | 
			
		||||
                    value={
 | 
			
		||||
                        "id": connection.id,
 | 
			
		||||
                        "displayName": group.name,
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ from typing import Optional
 | 
			
		||||
 | 
			
		||||
from pydanticscim.group import Group as BaseGroup
 | 
			
		||||
from pydanticscim.responses import PatchRequest as BasePatchRequest
 | 
			
		||||
from pydanticscim.responses import SCIMError as BaseSCIMError
 | 
			
		||||
from pydanticscim.service_provider import Bulk, ChangePassword, Filter, Patch
 | 
			
		||||
from pydanticscim.service_provider import (
 | 
			
		||||
    ServiceProviderConfiguration as BaseServiceProviderConfiguration,
 | 
			
		||||
@ -53,9 +52,3 @@ class PatchRequest(BasePatchRequest):
 | 
			
		||||
    """PatchRequest which correctly sets schemas"""
 | 
			
		||||
 | 
			
		||||
    schemas: tuple[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SCIMError(BaseSCIMError):
 | 
			
		||||
    """SCIM error with optional status code"""
 | 
			
		||||
 | 
			
		||||
    status: Optional[int]
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.lib.utils.errors import exception_to_string
 | 
			
		||||
from authentik.policies.utils import delete_none_values
 | 
			
		||||
from authentik.policies.utils import delete_none_keys
 | 
			
		||||
from authentik.providers.scim.clients.base import SCIMClient
 | 
			
		||||
from authentik.providers.scim.clients.exceptions import ResourceMissing, StopSync
 | 
			
		||||
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
 | 
			
		||||
@ -64,7 +64,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
 | 
			
		||||
        if not raw_scim_user:
 | 
			
		||||
            raise StopSync(ValueError("No user mappings configured"), obj)
 | 
			
		||||
        try:
 | 
			
		||||
            scim_user = SCIMUserSchema.parse_obj(delete_none_values(raw_scim_user))
 | 
			
		||||
            scim_user = SCIMUserSchema.parse_obj(delete_none_keys(raw_scim_user))
 | 
			
		||||
        except ValidationError as exc:
 | 
			
		||||
            raise StopSync(exc, obj) from exc
 | 
			
		||||
        if not scim_user.externalId:
 | 
			
		||||
 | 
			
		||||
@ -42,9 +42,7 @@ def scim_sync_all():
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
def scim_sync(self: MonitoredTask, provider_pk: int) -> None:
 | 
			
		||||
    """Run SCIM full sync for provider"""
 | 
			
		||||
    provider: SCIMProvider = SCIMProvider.objects.filter(
 | 
			
		||||
        pk=provider_pk, backchannel_application__isnull=False
 | 
			
		||||
    ).first()
 | 
			
		||||
    provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first()
 | 
			
		||||
    if not provider:
 | 
			
		||||
        return
 | 
			
		||||
    self.set_uid(slugify(provider.name))
 | 
			
		||||
@ -89,10 +87,10 @@ def scim_sync_users(page: int, provider_pk: int):
 | 
			
		||||
            LOGGER.warning("failed to sync user", exc=exc, user=user)
 | 
			
		||||
            messages.append(
 | 
			
		||||
                _(
 | 
			
		||||
                    "Failed to sync user %(user_name)s due to remote error: %(error)s"
 | 
			
		||||
                    "Failed to sync user due to remote error %(name)s: %(error)s"
 | 
			
		||||
                    % {
 | 
			
		||||
                        "user_name": user.username,
 | 
			
		||||
                        "error": exc.detail(),
 | 
			
		||||
                        "name": user.username,
 | 
			
		||||
                        "error": str(exc),
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@ -102,7 +100,7 @@ def scim_sync_users(page: int, provider_pk: int):
 | 
			
		||||
                _(
 | 
			
		||||
                    "Stopping sync due to error: %(error)s"
 | 
			
		||||
                    % {
 | 
			
		||||
                        "error": exc.detail(),
 | 
			
		||||
                        "error": str(exc),
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@ -130,10 +128,10 @@ def scim_sync_group(page: int, provider_pk: int):
 | 
			
		||||
            LOGGER.warning("failed to sync group", exc=exc, group=group)
 | 
			
		||||
            messages.append(
 | 
			
		||||
                _(
 | 
			
		||||
                    "Failed to sync group %(group_name)s due to remote error: %(error)s"
 | 
			
		||||
                    "Failed to sync group due to remote error %(name)s: %(error)s"
 | 
			
		||||
                    % {
 | 
			
		||||
                        "group_name": group.name,
 | 
			
		||||
                        "error": exc.detail(),
 | 
			
		||||
                        "name": group.name,
 | 
			
		||||
                        "error": str(exc),
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@ -143,7 +141,7 @@ def scim_sync_group(page: int, provider_pk: int):
 | 
			
		||||
                _(
 | 
			
		||||
                    "Stopping sync due to error: %(error)s"
 | 
			
		||||
                    % {
 | 
			
		||||
                        "error": exc.detail(),
 | 
			
		||||
                        "error": str(exc),
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,6 @@ class SCIMMembershipTests(TestCase):
 | 
			
		||||
            slug=generate_id(),
 | 
			
		||||
        )
 | 
			
		||||
        self.app.backchannel_providers.add(self.provider)
 | 
			
		||||
        self.provider.save()
 | 
			
		||||
        self.provider.property_mappings.set(
 | 
			
		||||
            [SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")]
 | 
			
		||||
        )
 | 
			
		||||
@ -92,6 +91,7 @@ class SCIMMembershipTests(TestCase):
 | 
			
		||||
                    "active": True,
 | 
			
		||||
                    "externalId": user.uid,
 | 
			
		||||
                    "name": {"familyName": "", "formatted": "", "givenName": ""},
 | 
			
		||||
                    "photos": [],
 | 
			
		||||
                    "displayName": "",
 | 
			
		||||
                    "userName": user.username,
 | 
			
		||||
                },
 | 
			
		||||
@ -177,6 +177,7 @@ class SCIMMembershipTests(TestCase):
 | 
			
		||||
                    "emails": [],
 | 
			
		||||
                    "externalId": user.uid,
 | 
			
		||||
                    "name": {"familyName": "", "formatted": "", "givenName": ""},
 | 
			
		||||
                    "photos": [],
 | 
			
		||||
                    "userName": user.username,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@ -81,6 +81,7 @@ class SCIMUserTests(TestCase):
 | 
			
		||||
                    "givenName": uid,
 | 
			
		||||
                },
 | 
			
		||||
                "displayName": uid,
 | 
			
		||||
                "photos": [],
 | 
			
		||||
                "userName": uid,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
@ -136,6 +137,7 @@ class SCIMUserTests(TestCase):
 | 
			
		||||
                    "formatted": uid,
 | 
			
		||||
                    "givenName": uid,
 | 
			
		||||
                },
 | 
			
		||||
                "photos": [],
 | 
			
		||||
                "userName": uid,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
@ -188,6 +190,7 @@ class SCIMUserTests(TestCase):
 | 
			
		||||
                    "givenName": uid,
 | 
			
		||||
                },
 | 
			
		||||
                "displayName": uid,
 | 
			
		||||
                "photos": [],
 | 
			
		||||
                "userName": uid,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
@ -255,6 +258,7 @@ class SCIMUserTests(TestCase):
 | 
			
		||||
                    "givenName": uid,
 | 
			
		||||
                },
 | 
			
		||||
                "displayName": uid,
 | 
			
		||||
                "photos": [],
 | 
			
		||||
                "userName": uid,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import importlib
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from hashlib import sha512
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from urllib.parse import quote_plus
 | 
			
		||||
 | 
			
		||||
import structlog
 | 
			
		||||
@ -20,9 +19,11 @@ from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BAC
 | 
			
		||||
 | 
			
		||||
LOGGER = structlog.get_logger()
 | 
			
		||||
 | 
			
		||||
BASE_DIR = Path(__file__).absolute().parent.parent.parent
 | 
			
		||||
STATICFILES_DIRS = [BASE_DIR / Path("web")]
 | 
			
		||||
MEDIA_ROOT = BASE_DIR / Path("media")
 | 
			
		||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 | 
			
		||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 | 
			
		||||
STATIC_ROOT = BASE_DIR + "/static"
 | 
			
		||||
STATICFILES_DIRS = [BASE_DIR + "/web"]
 | 
			
		||||
MEDIA_ROOT = BASE_DIR + "/media"
 | 
			
		||||
 | 
			
		||||
DEBUG = CONFIG.y_bool("debug")
 | 
			
		||||
SECRET_KEY = CONFIG.y("secret_key")
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ class LDAPBackend(InbuiltBackend):
 | 
			
		||||
        """Attempt authentication by binding to the LDAP server as `user`. This
 | 
			
		||||
        method should be avoided as its slow to do the bind."""
 | 
			
		||||
        # Try to bind as new user
 | 
			
		||||
        LOGGER.debug("Attempting to bind as user", user=user)
 | 
			
		||||
        LOGGER.debug("Attempting Binding as user", user=user)
 | 
			
		||||
        try:
 | 
			
		||||
            temp_connection = source.connection(
 | 
			
		||||
                connection_kwargs={
 | 
			
		||||
@ -65,8 +65,8 @@ class LDAPBackend(InbuiltBackend):
 | 
			
		||||
            )
 | 
			
		||||
            temp_connection.bind()
 | 
			
		||||
            return user
 | 
			
		||||
        except LDAPInvalidCredentialsResult as exc:
 | 
			
		||||
            LOGGER.debug("invalid LDAP credentials", user=user, exc=exc)
 | 
			
		||||
        except LDAPException as exc:
 | 
			
		||||
            LOGGER.warning("failed to bind to LDAP", exc=exc)
 | 
			
		||||
        except LDAPInvalidCredentialsResult as exception:
 | 
			
		||||
            LOGGER.debug("LDAPInvalidCredentialsResult", user=user, error=exception)
 | 
			
		||||
        except LDAPException as exception:
 | 
			
		||||
            LOGGER.warning(exception)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ from django.dispatch import receiver
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from ldap3.core.exceptions import LDAPOperationResult
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.core.signals import password_changed
 | 
			
		||||
@ -21,8 +20,6 @@ from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
 | 
			
		||||
from authentik.sources.ldap.tasks import ldap_sync
 | 
			
		||||
from authentik.stages.prompt.signals import password_validate
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=LDAPSource)
 | 
			
		||||
def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
 | 
			
		||||
@ -66,17 +63,13 @@ def ldap_sync_password(sender, user: User, password: str, **_):
 | 
			
		||||
    if not sources.exists():
 | 
			
		||||
        return
 | 
			
		||||
    source = sources.first()
 | 
			
		||||
    try:
 | 
			
		||||
    changer = LDAPPasswordChanger(source)
 | 
			
		||||
    try:
 | 
			
		||||
        changer.change_password(user, password)
 | 
			
		||||
    except LDAPOperationResult as exc:
 | 
			
		||||
        LOGGER.warning("failed to set LDAP password", exc=exc)
 | 
			
		||||
        Event.new(
 | 
			
		||||
            EventAction.CONFIGURATION_ERROR,
 | 
			
		||||
            message=(
 | 
			
		||||
                "Failed to change password in LDAP source due to remote error: "
 | 
			
		||||
                f"{exc.result}, {exc.message}, {exc.description}"
 | 
			
		||||
            ),
 | 
			
		||||
            message=f"Result: {exc.result}, Description {exc.description}",
 | 
			
		||||
            source=source,
 | 
			
		||||
        ).set_user(user).save()
 | 
			
		||||
        raise ValidationError("Failed to set password") from exc
 | 
			
		||||
 | 
			
		||||
@ -135,9 +135,9 @@ class BaseLDAPSynchronizer:
 | 
			
		||||
            if key == "attributes":
 | 
			
		||||
                continue
 | 
			
		||||
            setattr(instance, key, value)
 | 
			
		||||
        final_attributes = {}
 | 
			
		||||
        MERGE_LIST_UNIQUE.merge(final_attributes, instance.attributes)
 | 
			
		||||
        MERGE_LIST_UNIQUE.merge(final_attributes, data.get("attributes", {}))
 | 
			
		||||
        instance.attributes = final_attributes
 | 
			
		||||
        final_atttributes = {}
 | 
			
		||||
        MERGE_LIST_UNIQUE.merge(final_atttributes, instance.attributes)
 | 
			
		||||
        MERGE_LIST_UNIQUE.merge(final_atttributes, data.get("attributes", {}))
 | 
			
		||||
        instance.attributes = final_atttributes
 | 
			
		||||
        instance.save()
 | 
			
		||||
        return (instance, False)
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ from authentik.core.models import (
 | 
			
		||||
from authentik.core.sources.flow_manager import SourceFlowManager
 | 
			
		||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
			
		||||
from authentik.lib.utils.time import timedelta_from_string
 | 
			
		||||
from authentik.policies.utils import delete_none_values
 | 
			
		||||
from authentik.policies.utils import delete_none_keys
 | 
			
		||||
from authentik.sources.saml.exceptions import (
 | 
			
		||||
    InvalidSignature,
 | 
			
		||||
    MismatchedRequestID,
 | 
			
		||||
@ -160,7 +160,7 @@ class ResponseProcessor:
 | 
			
		||||
            self._source,
 | 
			
		||||
            self._http_request,
 | 
			
		||||
            name_id,
 | 
			
		||||
            delete_none_values(self.get_attributes()),
 | 
			
		||||
            delete_none_keys(self.get_attributes()),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _get_name_id(self) -> "Element":
 | 
			
		||||
@ -237,7 +237,7 @@ class ResponseProcessor:
 | 
			
		||||
            self._source,
 | 
			
		||||
            self._http_request,
 | 
			
		||||
            name_id.text,
 | 
			
		||||
            delete_none_values(self.get_attributes()),
 | 
			
		||||
            delete_none_keys(self.get_attributes()),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,7 @@ class AuthenticatorSMSStage(ConfigurableStage, FriendlyNamedStage, Stage):
 | 
			
		||||
            "From": self.from_number,
 | 
			
		||||
            "To": device.phone_number,
 | 
			
		||||
            "Body": token,
 | 
			
		||||
            "Message": str(self.get_message(token)),
 | 
			
		||||
            "Message": self.get_message(token),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.mapping:
 | 
			
		||||
 | 
			
		||||
@ -133,12 +133,6 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
 | 
			
		||||
    device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
 | 
			
		||||
    if not device:
 | 
			
		||||
        raise ValidationError("Invalid device")
 | 
			
		||||
    # We can only check the device's user if the user we're given isn't anonymous
 | 
			
		||||
    # as this validation is also used for password-less login where webauthn is the very first
 | 
			
		||||
    # step done by a user. Only if this validation happens at a later stage we can check
 | 
			
		||||
    # that the device belongs to the user
 | 
			
		||||
    if not user.is_anonymous and device.user != user:
 | 
			
		||||
        raise ValidationError("Invalid device")
 | 
			
		||||
 | 
			
		||||
    stage: AuthenticatorValidateStage = stage_view.executor.current_stage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -36,9 +36,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
 | 
			
		||||
 | 
			
		||||
COOKIE_NAME_MFA = "authentik_mfa"
 | 
			
		||||
 | 
			
		||||
PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
 | 
			
		||||
PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
 | 
			
		||||
PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
 | 
			
		||||
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages"
 | 
			
		||||
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage"
 | 
			
		||||
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SelectableStageSerializer(PassiveSerializer):
 | 
			
		||||
@ -72,8 +72,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
 | 
			
		||||
    component = CharField(default="ak-stage-authenticator-validate")
 | 
			
		||||
 | 
			
		||||
    def _challenge_allowed(self, classes: list):
 | 
			
		||||
        device_challenges: list[dict] = self.stage.executor.plan.context.get(
 | 
			
		||||
            PLAN_CONTEXT_DEVICE_CHALLENGES, []
 | 
			
		||||
        device_challenges: list[dict] = self.stage.request.session.get(
 | 
			
		||||
            SESSION_KEY_DEVICE_CHALLENGES, []
 | 
			
		||||
        )
 | 
			
		||||
        if not any(x["device_class"] in classes for x in device_challenges):
 | 
			
		||||
            raise ValidationError("No compatible device class allowed")
 | 
			
		||||
@ -103,9 +103,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
 | 
			
		||||
        """Check which challenge the user has selected. Actual logic only used for SMS stage."""
 | 
			
		||||
        # First check if the challenge is valid
 | 
			
		||||
        allowed = False
 | 
			
		||||
        for device_challenge in self.stage.executor.plan.context.get(
 | 
			
		||||
            PLAN_CONTEXT_DEVICE_CHALLENGES, []
 | 
			
		||||
        ):
 | 
			
		||||
        for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []):
 | 
			
		||||
            if device_challenge.get("device_class", "") == challenge.get(
 | 
			
		||||
                "device_class", ""
 | 
			
		||||
            ) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
 | 
			
		||||
@ -123,11 +121,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
 | 
			
		||||
 | 
			
		||||
    def validate_selected_stage(self, stage_pk: str) -> str:
 | 
			
		||||
        """Check that the selected stage is valid"""
 | 
			
		||||
        stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
 | 
			
		||||
        stages = self.stage.request.session.get(SESSION_KEY_STAGES, [])
 | 
			
		||||
        if not any(str(stage.pk) == stage_pk for stage in stages):
 | 
			
		||||
            raise ValidationError("Selected stage is invalid")
 | 
			
		||||
        self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
 | 
			
		||||
        self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
 | 
			
		||||
        self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk
 | 
			
		||||
        return stage_pk
 | 
			
		||||
 | 
			
		||||
    def validate(self, attrs: dict):
 | 
			
		||||
@ -232,7 +230,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
			
		||||
            else:
 | 
			
		||||
                self.logger.debug("No pending user, continuing")
 | 
			
		||||
                return self.executor.stage_ok()
 | 
			
		||||
        self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
 | 
			
		||||
        self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges
 | 
			
		||||
 | 
			
		||||
        # No allowed devices
 | 
			
		||||
        if len(challenges) < 1:
 | 
			
		||||
@ -265,23 +263,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
			
		||||
        if stage.configuration_stages.count() == 1:
 | 
			
		||||
            next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
 | 
			
		||||
            self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
 | 
			
		||||
            self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
 | 
			
		||||
            self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage
 | 
			
		||||
            # Because that normal execution only happens on post, we directly inject it here and
 | 
			
		||||
            # return it
 | 
			
		||||
            self.executor.plan.insert_stage(next_stage)
 | 
			
		||||
            return self.executor.stage_ok()
 | 
			
		||||
        stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
 | 
			
		||||
        self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
 | 
			
		||||
        self.request.session[SESSION_KEY_STAGES] = stages
 | 
			
		||||
        return super().get(self.request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
			
		||||
        res = super().post(request, *args, **kwargs)
 | 
			
		||||
        if (
 | 
			
		||||
            PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
 | 
			
		||||
            SESSION_KEY_SELECTED_STAGE in self.request.session
 | 
			
		||||
            and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
 | 
			
		||||
        ):
 | 
			
		||||
            self.logger.debug("Got selected stage in context, running that")
 | 
			
		||||
            stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
 | 
			
		||||
            self.logger.debug("Got selected stage in session, running that")
 | 
			
		||||
            stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE)
 | 
			
		||||
            # Because the foreign key to stage.configuration_stage points to
 | 
			
		||||
            # a base stage class, we need to do another lookup
 | 
			
		||||
            stage = Stage.objects.get_subclass(pk=stage_pk)
 | 
			
		||||
@ -292,8 +290,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
			
		||||
        return res
 | 
			
		||||
 | 
			
		||||
    def get_challenge(self) -> AuthenticatorValidationChallenge:
 | 
			
		||||
        challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
 | 
			
		||||
        stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
 | 
			
		||||
        challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, [])
 | 
			
		||||
        stages = self.request.session.get(SESSION_KEY_STAGES, [])
 | 
			
		||||
        stage_challenges = []
 | 
			
		||||
        for stage in stages:
 | 
			
		||||
            serializer = SelectableStageSerializer(
 | 
			
		||||
@ -308,7 +306,6 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
			
		||||
            stage_challenges.append(serializer.data)
 | 
			
		||||
        return AuthenticatorValidationChallenge(
 | 
			
		||||
            data={
 | 
			
		||||
                "component": "ak-stage-authenticator-validate",
 | 
			
		||||
                "type": ChallengeTypes.NATIVE.value,
 | 
			
		||||
                "device_challenges": challenges,
 | 
			
		||||
                "configuration_stages": stage_challenges,
 | 
			
		||||
@ -388,3 +385,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
 | 
			
		||||
                "device": webauthn_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,19 +1,26 @@
 | 
			
		||||
"""Test validator stage"""
 | 
			
		||||
from unittest.mock import MagicMock, patch
 | 
			
		||||
 | 
			
		||||
from django.contrib.sessions.middleware import SessionMiddleware
 | 
			
		||||
from django.test.client import RequestFactory
 | 
			
		||||
from django.urls.base import reverse
 | 
			
		||||
from rest_framework.exceptions import ValidationError
 | 
			
		||||
 | 
			
		||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
			
		||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
 | 
			
		||||
from authentik.flows.planner import FlowPlan
 | 
			
		||||
from authentik.flows.stage import StageView
 | 
			
		||||
from authentik.flows.tests import FlowTestCase
 | 
			
		||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
			
		||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
 | 
			
		||||
from authentik.lib.generators import generate_id, generate_key
 | 
			
		||||
from authentik.lib.tests.utils import dummy_get_response
 | 
			
		||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
 | 
			
		||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
 | 
			
		||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
 | 
			
		||||
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
 | 
			
		||||
from authentik.stages.authenticator_validate.stage import (
 | 
			
		||||
    SESSION_KEY_DEVICE_CHALLENGES,
 | 
			
		||||
    AuthenticatorValidationChallengeResponse,
 | 
			
		||||
)
 | 
			
		||||
from authentik.stages.identification.models import IdentificationStage, UserFields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -79,17 +86,12 @@ class AuthenticatorValidateStageTests(FlowTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_validate_selected_challenge(self):
 | 
			
		||||
        """Test validate_selected_challenge"""
 | 
			
		||||
        flow = create_test_flow()
 | 
			
		||||
        stage = AuthenticatorValidateStage.objects.create(
 | 
			
		||||
            name=generate_id(),
 | 
			
		||||
            not_configured_action=NotConfiguredAction.CONFIGURE,
 | 
			
		||||
            device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
 | 
			
		||||
        )
 | 
			
		||||
        # Prepare request with session
 | 
			
		||||
        request = self.request_factory.get("/")
 | 
			
		||||
 | 
			
		||||
        session = self.client.session
 | 
			
		||||
        plan = FlowPlan(flow_pk=flow.pk.hex)
 | 
			
		||||
        plan.append_stage(stage)
 | 
			
		||||
        plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
 | 
			
		||||
        middleware = SessionMiddleware(dummy_get_response)
 | 
			
		||||
        middleware.process_request(request)
 | 
			
		||||
        request.session[SESSION_KEY_DEVICE_CHALLENGES] = [
 | 
			
		||||
            {
 | 
			
		||||
                "device_class": "static",
 | 
			
		||||
                "device_uid": "1",
 | 
			
		||||
@ -99,43 +101,23 @@ class AuthenticatorValidateStageTests(FlowTestCase):
 | 
			
		||||
                "device_uid": "2",
 | 
			
		||||
            },
 | 
			
		||||
        ]
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        session.save()
 | 
			
		||||
        request.session.save()
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
			
		||||
            data={
 | 
			
		||||
                "selected_challenge": {
 | 
			
		||||
        res = AuthenticatorValidationChallengeResponse()
 | 
			
		||||
        res.stage = StageView(FlowExecutorView())
 | 
			
		||||
        res.stage.request = request
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            res.validate_selected_challenge(
 | 
			
		||||
                {
 | 
			
		||||
                    "device_class": "baz",
 | 
			
		||||
                    "device_uid": "quox",
 | 
			
		||||
                    "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": {
 | 
			
		||||
        res.validate_selected_challenge(
 | 
			
		||||
            {
 | 
			
		||||
                "device_class": "static",
 | 
			
		||||
                "device_uid": "1",
 | 
			
		||||
                    "challenge": {},
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        self.assertStageResponse(
 | 
			
		||||
            response,
 | 
			
		||||
            flow,
 | 
			
		||||
            response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
 | 
			
		||||
            component="ak-stage-authenticator-validate",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch(
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
 | 
			
		||||
)
 | 
			
		||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
 | 
			
		||||
from authentik.stages.authenticator_validate.stage import (
 | 
			
		||||
    PLAN_CONTEXT_DEVICE_CHALLENGES,
 | 
			
		||||
    SESSION_KEY_DEVICE_CHALLENGES,
 | 
			
		||||
    AuthenticatorValidateStageView,
 | 
			
		||||
)
 | 
			
		||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
 | 
			
		||||
@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
			
		||||
        plan.append_stage(stage)
 | 
			
		||||
        plan.append_stage(UserLoginStage(name=generate_id()))
 | 
			
		||||
        plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
 | 
			
		||||
        plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        session[SESSION_KEY_DEVICE_CHALLENGES] = [
 | 
			
		||||
            {
 | 
			
		||||
                "device_class": device.__class__.__name__.lower().replace("device", ""),
 | 
			
		||||
                "device_uid": device.pk,
 | 
			
		||||
                "challenge": {},
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
			
		||||
            "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
 | 
			
		||||
        )
 | 
			
		||||
@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
 | 
			
		||||
        plan = FlowPlan(flow_pk=flow.pk.hex)
 | 
			
		||||
        plan.append_stage(stage)
 | 
			
		||||
        plan.append_stage(UserLoginStage(name=generate_id()))
 | 
			
		||||
        plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        session[SESSION_KEY_DEVICE_CHALLENGES] = [
 | 
			
		||||
            {
 | 
			
		||||
                "device_class": device.__class__.__name__.lower().replace("device", ""),
 | 
			
		||||
                "device_uid": device.pk,
 | 
			
		||||
                "challenge": {},
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
 | 
			
		||||
            "g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ from authentik.flows.models import Stage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DenyStage(Stage):
 | 
			
		||||
    """Cancels the current flow."""
 | 
			
		||||
    """Cancells the current flow."""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serializer(self) -> type[BaseSerializer]:
 | 
			
		||||
 | 
			
		||||
@ -5,10 +5,10 @@ from authentik.flows.stage import StageView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DenyStageView(StageView):
 | 
			
		||||
    """Cancels the current flow"""
 | 
			
		||||
    """Cancells the current flow"""
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        """Cancels the current flow"""
 | 
			
		||||
        """Cancells the current flow"""
 | 
			
		||||
        return self.executor.stage_invalid()
 | 
			
		||||
 | 
			
		||||
    def post(self, request: HttpRequest) -> HttpResponse:
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ from rest_framework.fields import CharField
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
 | 
			
		||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
 | 
			
		||||
from authentik.flows.models import FlowDesignation, FlowToken
 | 
			
		||||
from authentik.flows.models import FlowToken
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
			
		||||
from authentik.flows.stage import ChallengeStageView
 | 
			
		||||
from authentik.flows.views.executor import QS_KEY_TOKEN
 | 
			
		||||
@ -82,11 +82,6 @@ class EmailStageView(ChallengeStageView):
 | 
			
		||||
        """Helper function that sends the actual email. Implies that you've
 | 
			
		||||
        already checked that there is a pending user."""
 | 
			
		||||
        pending_user = self.get_pending_user()
 | 
			
		||||
        if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
 | 
			
		||||
            # Pending user does not have a primary key, and we're in a recovery flow,
 | 
			
		||||
            # which means the user entered an invalid identifier, so we pretend to send the
 | 
			
		||||
            # email, to not disclose if the user exists
 | 
			
		||||
            return
 | 
			
		||||
        email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
 | 
			
		||||
        if not email:
 | 
			
		||||
            email = pending_user.email
 | 
			
		||||
 | 
			
		||||
@ -5,20 +5,18 @@ from unittest.mock import MagicMock, PropertyMock, patch
 | 
			
		||||
from django.core import mail
 | 
			
		||||
from django.core.mail.backends.locmem import EmailBackend
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from rest_framework.test import APITestCase
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.flows.markers import StageMarker
 | 
			
		||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
			
		||||
from authentik.flows.tests import FlowTestCase
 | 
			
		||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
from authentik.stages.email.models import EmailStage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestEmailStageSending(FlowTestCase):
 | 
			
		||||
class TestEmailStageSending(APITestCase):
 | 
			
		||||
    """Email tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@ -46,13 +44,6 @@ class TestEmailStageSending(FlowTestCase):
 | 
			
		||||
        ):
 | 
			
		||||
            response = self.client.post(url)
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
            self.assertStageResponse(
 | 
			
		||||
                response,
 | 
			
		||||
                self.flow,
 | 
			
		||||
                response_errors={
 | 
			
		||||
                    "non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            self.assertEqual(len(mail.outbox), 1)
 | 
			
		||||
            self.assertEqual(mail.outbox[0].subject, "authentik")
 | 
			
		||||
            events = Event.objects.filter(action=EventAction.EMAIL_SENT)
 | 
			
		||||
@ -63,32 +54,6 @@ class TestEmailStageSending(FlowTestCase):
 | 
			
		||||
            self.assertEqual(event.context["to_email"], [self.user.email])
 | 
			
		||||
            self.assertEqual(event.context["from_email"], "system@authentik.local")
 | 
			
		||||
 | 
			
		||||
    def test_pending_fake_user(self):
 | 
			
		||||
        """Test with pending (fake) user"""
 | 
			
		||||
        self.flow.designation = FlowDesignation.RECOVERY
 | 
			
		||||
        self.flow.save()
 | 
			
		||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
			
		||||
        plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
 | 
			
		||||
        session = self.client.session
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        session.save()
 | 
			
		||||
 | 
			
		||||
        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
			
		||||
        with patch(
 | 
			
		||||
            "authentik.stages.email.models.EmailStage.backend_class",
 | 
			
		||||
            PropertyMock(return_value=EmailBackend),
 | 
			
		||||
        ):
 | 
			
		||||
            response = self.client.post(url)
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
            self.assertStageResponse(
 | 
			
		||||
                response,
 | 
			
		||||
                self.flow,
 | 
			
		||||
                response_errors={
 | 
			
		||||
                    "non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            self.assertEqual(len(mail.outbox), 0)
 | 
			
		||||
 | 
			
		||||
    def test_send_error(self):
 | 
			
		||||
        """Test error during sending (sending will be retried)"""
 | 
			
		||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
			
		||||
 | 
			
		||||
@ -118,12 +118,8 @@ class IdentificationChallengeResponse(ChallengeResponse):
 | 
			
		||||
                username=uid_field,
 | 
			
		||||
                email=uid_field,
 | 
			
		||||
            )
 | 
			
		||||
            self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
 | 
			
		||||
            if not current_stage.show_matched_user:
 | 
			
		||||
                self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
 | 
			
		||||
            if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
 | 
			
		||||
                # When used in a recovery flow, always continue to not disclose if a user exists
 | 
			
		||||
                return attrs
 | 
			
		||||
            raise ValidationError("Failed to authenticate.")
 | 
			
		||||
        self.pre_user = pre_user
 | 
			
		||||
        if not current_stage.password_stage:
 | 
			
		||||
 | 
			
		||||
@ -188,7 +188,7 @@ class TestIdentificationStage(FlowTestCase):
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_link_recovery_flow(self):
 | 
			
		||||
    def test_recovery_flow(self):
 | 
			
		||||
        """Test that recovery flow is linked correctly"""
 | 
			
		||||
        flow = create_test_flow()
 | 
			
		||||
        self.stage.recovery_flow = flow
 | 
			
		||||
@ -226,38 +226,6 @@ class TestIdentificationStage(FlowTestCase):
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_recovery_flow_invalid_user(self):
 | 
			
		||||
        """Test that an invalid user can proceed in a recovery flow"""
 | 
			
		||||
        self.flow.designation = FlowDesignation.RECOVERY
 | 
			
		||||
        self.flow.save()
 | 
			
		||||
        response = self.client.get(
 | 
			
		||||
            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 | 
			
		||||
        )
 | 
			
		||||
        self.assertStageResponse(
 | 
			
		||||
            response,
 | 
			
		||||
            self.flow,
 | 
			
		||||
            component="ak-stage-identification",
 | 
			
		||||
            user_fields=["email"],
 | 
			
		||||
            password_fields=False,
 | 
			
		||||
            show_source_labels=False,
 | 
			
		||||
            primary_action="Continue",
 | 
			
		||||
            sources=[
 | 
			
		||||
                {
 | 
			
		||||
                    "challenge": {
 | 
			
		||||
                        "component": "xak-flow-redirect",
 | 
			
		||||
                        "to": "/source/oauth/login/test/",
 | 
			
		||||
                        "type": ChallengeTypes.REDIRECT.value,
 | 
			
		||||
                    },
 | 
			
		||||
                    "icon_url": "/static/authentik/sources/default.svg",
 | 
			
		||||
                    "name": "test",
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
        form_data = {"uid_field": generate_id()}
 | 
			
		||||
        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
			
		||||
        response = self.client.post(url, form_data)
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_api_validate(self):
 | 
			
		||||
        """Test API validation"""
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ from django.db import transaction
 | 
			
		||||
from django.db.utils import IntegrityError, InternalError
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from rest_framework.exceptions import ValidationError
 | 
			
		||||
 | 
			
		||||
from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
 | 
			
		||||
from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection
 | 
			
		||||
@ -149,11 +148,7 @@ class UserWriteStageView(StageView):
 | 
			
		||||
            and SESSION_KEY_IMPERSONATE_USER not in self.request.session
 | 
			
		||||
        ):
 | 
			
		||||
            should_update_session = True
 | 
			
		||||
        try:
 | 
			
		||||
        self.update_user(user)
 | 
			
		||||
        except ValidationError as exc:
 | 
			
		||||
            self.logger.warning("failed to update user", exc=exc)
 | 
			
		||||
            return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
 | 
			
		||||
        # Extra check to prevent flows from saving a user with a blank username
 | 
			
		||||
        if user.username == "":
 | 
			
		||||
            self.logger.warning("Aborting write to empty username", user=user)
 | 
			
		||||
@ -167,7 +162,7 @@ class UserWriteStageView(StageView):
 | 
			
		||||
                    user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
 | 
			
		||||
        except (IntegrityError, ValueError, TypeError, InternalError) as exc:
 | 
			
		||||
            self.logger.warning("Failed to save user", exc=exc)
 | 
			
		||||
            return self.executor.stage_invalid(_("Failed to update user. Please try again later."))
 | 
			
		||||
            return self.executor.stage_invalid(_("Failed to save user"))
 | 
			
		||||
        user_write.send(sender=self, request=request, user=user, data=data, created=user_created)
 | 
			
		||||
        # Check if the password has been updated, and update the session auth hash
 | 
			
		||||
        if should_update_session:
 | 
			
		||||
 | 
			
		||||
@ -2560,42 +2560,6 @@
 | 
			
		||||
                                "$ref": "#/$defs/model_authentik_core.token"
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "object",
 | 
			
		||||
                        "required": [
 | 
			
		||||
                            "model",
 | 
			
		||||
                            "identifiers"
 | 
			
		||||
                        ],
 | 
			
		||||
                        "properties": {
 | 
			
		||||
                            "model": {
 | 
			
		||||
                                "const": "authentik_blueprints.metaapplyblueprint"
 | 
			
		||||
                            },
 | 
			
		||||
                            "id": {
 | 
			
		||||
                                "type": "string"
 | 
			
		||||
                            },
 | 
			
		||||
                            "state": {
 | 
			
		||||
                                "type": "string",
 | 
			
		||||
                                "enum": [
 | 
			
		||||
                                    "absent",
 | 
			
		||||
                                    "present",
 | 
			
		||||
                                    "created"
 | 
			
		||||
                                ],
 | 
			
		||||
                                "default": "present"
 | 
			
		||||
                            },
 | 
			
		||||
                            "conditions": {
 | 
			
		||||
                                "type": "array",
 | 
			
		||||
                                "items": {
 | 
			
		||||
                                    "type": "boolean"
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            "attrs": {
 | 
			
		||||
                                "$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
 | 
			
		||||
                            },
 | 
			
		||||
                            "identifiers": {
 | 
			
		||||
                                "$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
@ -3888,7 +3852,8 @@
 | 
			
		||||
                    },
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "username",
 | 
			
		||||
                        "name"
 | 
			
		||||
                        "name",
 | 
			
		||||
                        "groups"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "title": "User"
 | 
			
		||||
                },
 | 
			
		||||
@ -4079,7 +4044,8 @@
 | 
			
		||||
                    },
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "username",
 | 
			
		||||
                        "name"
 | 
			
		||||
                        "name",
 | 
			
		||||
                        "groups"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "title": "User"
 | 
			
		||||
                },
 | 
			
		||||
@ -4274,7 +4240,8 @@
 | 
			
		||||
                    },
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "username",
 | 
			
		||||
                        "name"
 | 
			
		||||
                        "name",
 | 
			
		||||
                        "groups"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "title": "User"
 | 
			
		||||
                },
 | 
			
		||||
@ -6416,7 +6383,8 @@
 | 
			
		||||
                    },
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "username",
 | 
			
		||||
                        "name"
 | 
			
		||||
                        "name",
 | 
			
		||||
                        "groups"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "title": "User"
 | 
			
		||||
                },
 | 
			
		||||
@ -7151,7 +7119,8 @@
 | 
			
		||||
                    },
 | 
			
		||||
                    "required": [
 | 
			
		||||
                        "username",
 | 
			
		||||
                        "name"
 | 
			
		||||
                        "name",
 | 
			
		||||
                        "groups"
 | 
			
		||||
                    ],
 | 
			
		||||
                    "title": "User"
 | 
			
		||||
                },
 | 
			
		||||
@ -8345,21 +8314,6 @@
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "required": []
 | 
			
		||||
        },
 | 
			
		||||
        "model_authentik_blueprints.metaapplyblueprint": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "identifiers": {
 | 
			
		||||
                    "type": "object",
 | 
			
		||||
                    "additionalProperties": true,
 | 
			
		||||
                    "title": "Identifiers"
 | 
			
		||||
                },
 | 
			
		||||
                "required": {
 | 
			
		||||
                    "type": "boolean",
 | 
			
		||||
                    "title": "Required"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "required": []
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ entries:
 | 
			
		||||
 | 
			
		||||
        # photos supports URLs to images, however authentik might return data URIs
 | 
			
		||||
        avatar = request.user.avatar
 | 
			
		||||
        photos = None
 | 
			
		||||
        photos = []
 | 
			
		||||
        if "://" in avatar:
 | 
			
		||||
            photos = [{"value": avatar, "type": "photo"}]
 | 
			
		||||
 | 
			
		||||
@ -31,11 +31,11 @@ entries:
 | 
			
		||||
 | 
			
		||||
        emails = []
 | 
			
		||||
        if request.user.email != "":
 | 
			
		||||
            emails = [{
 | 
			
		||||
            emails.append({
 | 
			
		||||
                "value": request.user.email,
 | 
			
		||||
                "type": "other",
 | 
			
		||||
                "primary": True,
 | 
			
		||||
            }]
 | 
			
		||||
            })
 | 
			
		||||
        return {
 | 
			
		||||
            "userName": request.user.username,
 | 
			
		||||
            "name": {
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ services:
 | 
			
		||||
    volumes:
 | 
			
		||||
      - redis:/data
 | 
			
		||||
  server:
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.6}
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.1}
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: server
 | 
			
		||||
    environment:
 | 
			
		||||
@ -50,7 +50,7 @@ services:
 | 
			
		||||
      - "${COMPOSE_PORT_HTTP:-9000}:9000"
 | 
			
		||||
      - "${COMPOSE_PORT_HTTPS:-9443}:9443"
 | 
			
		||||
  worker:
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.6}
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.1}
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: worker
 | 
			
		||||
    environment:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							@ -23,10 +23,10 @@ require (
 | 
			
		||||
	github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
 | 
			
		||||
	github.com/pires/go-proxyproto v0.7.0
 | 
			
		||||
	github.com/prometheus/client_golang v1.15.1
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.2
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.0
 | 
			
		||||
	github.com/spf13/cobra v1.7.0
 | 
			
		||||
	github.com/stretchr/testify v1.8.2
 | 
			
		||||
	goauthentik.io/api/v3 v3.2023050.2
 | 
			
		||||
	goauthentik.io/api/v3 v3.2023041.12
 | 
			
		||||
	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
 | 
			
		||||
	golang.org/x/oauth2 v0.8.0
 | 
			
		||||
	golang.org/x/sync v0.2.0
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							@ -200,8 +200,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 | 
			
		||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 | 
			
		||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
 | 
			
		||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 | 
			
		||||
@ -241,8 +241,8 @@ go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvx
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
 | 
			
		||||
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
 | 
			
		||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
 | 
			
		||||
goauthentik.io/api/v3 v3.2023050.2 h1:EnwEaPM2qSFwfow0G/pTk9GHXmux0ldN77b+/gMeGTM=
 | 
			
		||||
goauthentik.io/api/v3 v3.2023050.2/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
 | 
			
		||||
goauthentik.io/api/v3 v3.2023041.12 h1:lk8eCWYW/P8U4r10RgtIq2NyaAqZ3KKrKc7eierV6aY=
 | 
			
		||||
goauthentik.io/api/v3 v3.2023041.12/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,6 @@ type ListenConfig struct {
 | 
			
		||||
	Radius  string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
 | 
			
		||||
	Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
 | 
			
		||||
	Debug   string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
 | 
			
		||||
	TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PathsConfig struct {
 | 
			
		||||
 | 
			
		||||
@ -29,4 +29,4 @@ func UserAgent() string {
 | 
			
		||||
	return fmt.Sprintf("authentik@%s", FullVersion())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const VERSION = "2023.5.6"
 | 
			
		||||
const VERSION = "2023.4.1"
 | 
			
		||||
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/handlers"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"goauthentik.io/internal/config"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ProxyHeaders Set proxy headers like X-Forwarded-For and such, but only if the direct connection
 | 
			
		||||
// comes from a client that's in a list of trusted CIDRs
 | 
			
		||||
func ProxyHeaders() func(http.Handler) http.Handler {
 | 
			
		||||
	nets := []*net.IPNet{}
 | 
			
		||||
	for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
 | 
			
		||||
		_, cidr, err := net.ParseCIDR(rn)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		nets = append(nets, cidr)
 | 
			
		||||
	}
 | 
			
		||||
	ph := handlers.ProxyHeaders
 | 
			
		||||
	return func(h http.Handler) http.Handler {
 | 
			
		||||
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			host, _, err := net.SplitHostPort(r.RemoteAddr)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				// remoteAddr will be nil if the IP cannot be parsed
 | 
			
		||||
				remoteAddr := net.ParseIP(host)
 | 
			
		||||
				for _, allowedCidr := range nets {
 | 
			
		||||
					if remoteAddr != nil && allowedCidr.Contains(remoteAddr) {
 | 
			
		||||
						log.WithField("remoteAddr", remoteAddr).WithField("cidr", allowedCidr.String()).Trace("Setting proxy headers")
 | 
			
		||||
						ph(h).ServeHTTP(w, r)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// Request is not directly coming from a CIDR we "trust"
 | 
			
		||||
			// so set XFF to the direct host IP
 | 
			
		||||
			r.Header.Set("X-Forwarded-For", host)
 | 
			
		||||
			h.ServeHTTP(w, r)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -35,7 +35,7 @@ type WebServer struct {
 | 
			
		||||
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
 | 
			
		||||
	l := log.WithField("logger", "authentik.router")
 | 
			
		||||
	mainHandler := mux.NewRouter()
 | 
			
		||||
	mainHandler.Use(web.ProxyHeaders())
 | 
			
		||||
	mainHandler.Use(handlers.ProxyHeaders)
 | 
			
		||||
	mainHandler.Use(handlers.CompressHandler)
 | 
			
		||||
	loggingHandler := mainHandler.NewRoute().Subrouter()
 | 
			
		||||
	loggingHandler.Use(web.NewLoggingHandler(l, nil))
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2023-05-18 14:21+0000\n"
 | 
			
		||||
"POT-Creation-Date: 2023-05-10 17:31+0000\n"
 | 
			
		||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
			
		||||
@ -1381,33 +1381,33 @@ msgstr ""
 | 
			
		||||
msgid "SCIM Mappings"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/providers/scim/tasks.py:52
 | 
			
		||||
#: authentik/providers/scim/tasks.py:50
 | 
			
		||||
msgid "Starting full SCIM sync"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/providers/scim/tasks.py:59
 | 
			
		||||
#: authentik/providers/scim/tasks.py:57
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Syncing page %(page)d of users"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/providers/scim/tasks.py:63
 | 
			
		||||
#: authentik/providers/scim/tasks.py:61
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Syncing page %(page)d of groups"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/providers/scim/tasks.py:92
 | 
			
		||||
#: authentik/providers/scim/tasks.py:90
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Failed to sync user %(user_name)s due to remote error: %(error)s"
 | 
			
		||||
msgid "Failed to sync user due to remote error %(name)s: %(error)s"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/providers/scim/tasks.py:103 authentik/providers/scim/tasks.py:144
 | 
			
		||||
#: authentik/providers/scim/tasks.py:101 authentik/providers/scim/tasks.py:142
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Stopping sync due to error: %(error)s"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/providers/scim/tasks.py:133
 | 
			
		||||
#: authentik/providers/scim/tasks.py:131
 | 
			
		||||
#, python-format
 | 
			
		||||
msgid "Failed to sync group %(group_name)s due to remote error: %(error)s"
 | 
			
		||||
msgid "Failed to sync group due to remote error %(name)s: %(error)s"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/recovery/management/commands/create_admin_group.py:11
 | 
			
		||||
@ -2106,10 +2106,6 @@ msgid ""
 | 
			
		||||
"                    "
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/api.py:20
 | 
			
		||||
msgid "When no user fields are selected, at least one source must be selected"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py:29
 | 
			
		||||
msgid ""
 | 
			
		||||
"Fields of the user object to match against. (Hold shift to select multiple "
 | 
			
		||||
@ -2401,17 +2397,16 @@ msgstr ""
 | 
			
		||||
msgid "User Write Stages"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/user_write/stage.py:133
 | 
			
		||||
#: authentik/stages/user_write/stage.py:132
 | 
			
		||||
msgid "No Pending data."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/user_write/stage.py:139
 | 
			
		||||
#: authentik/stages/user_write/stage.py:138
 | 
			
		||||
msgid "No user found and can't create new user."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/user_write/stage.py:156
 | 
			
		||||
#: authentik/stages/user_write/stage.py:170
 | 
			
		||||
msgid "Failed to update user. Please try again later."
 | 
			
		||||
#: authentik/stages/user_write/stage.py:165
 | 
			
		||||
msgid "Failed to save user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py:23
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										160
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										160
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@ -878,63 +878,63 @@ files = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "coverage"
 | 
			
		||||
version = "7.2.6"
 | 
			
		||||
version = "7.2.5"
 | 
			
		||||
description = "Code coverage measurement for Python"
 | 
			
		||||
category = "dev"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.7"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"},
 | 
			
		||||
    {file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"},
 | 
			
		||||
    {file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"},
 | 
			
		||||
    {file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"},
 | 
			
		||||
    {file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"},
 | 
			
		||||
    {file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"},
 | 
			
		||||
    {file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
@ -988,13 +988,14 @@ tox = ["tox"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dacite"
 | 
			
		||||
version = "1.8.1"
 | 
			
		||||
version = "1.8.0"
 | 
			
		||||
description = "Simple creation of data classes from dictionaries."
 | 
			
		||||
category = "main"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.6"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "dacite-1.8.1-py3-none-any.whl", hash = "sha256:cc31ad6fdea1f49962ea42db9421772afe01ac5442380d9a99fcf3d188c61afe"},
 | 
			
		||||
    {file = "dacite-1.8.0-py3-none-any.whl", hash = "sha256:f7b1205cc5d9b62835aac8cbc1e6e37c1da862359a401f1edbe2ae08fbdc6193"},
 | 
			
		||||
    {file = "dacite-1.8.0.tar.gz", hash = "sha256:6257a5e505b61a8cafee7ef3ad08cf32ee9b885718f42395d017e0a9b4c6af65"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
@ -1251,14 +1252,14 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "docker"
 | 
			
		||||
version = "6.1.2"
 | 
			
		||||
version = "6.1.1"
 | 
			
		||||
description = "A Python library for the Docker Engine API."
 | 
			
		||||
category = "main"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.7"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "docker-6.1.2-py3-none-any.whl", hash = "sha256:134cd828f84543cbf8e594ff81ca90c38288df3c0a559794c12f2e4b634ea19e"},
 | 
			
		||||
    {file = "docker-6.1.2.tar.gz", hash = "sha256:dcc088adc2ec4e7cfc594e275d8bd2c9738c56c808de97476939ef67db5af8c2"},
 | 
			
		||||
    {file = "docker-6.1.1-py3-none-any.whl", hash = "sha256:8308b23d3d0982c74f7aa0a3abd774898c0c4fba006e9c3bde4f68354e470fe2"},
 | 
			
		||||
    {file = "docker-6.1.1.tar.gz", hash = "sha256:5ec18b9c49d48ee145a5b5824bb126dc32fc77931e18444783fc07a7724badc0"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
@ -3108,29 +3109,29 @@ pyasn1 = ">=0.1.3"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ruff"
 | 
			
		||||
version = "0.0.267"
 | 
			
		||||
version = "0.0.265"
 | 
			
		||||
description = "An extremely fast Python linter, written in Rust."
 | 
			
		||||
category = "dev"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.7"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:4adbbbe314d8fcc539a245065bad89446a3cef2e0c9cf70bf7bb9ed6fe31856d"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:67254ae34c38cba109fdc52e4a70887de1f850fb3971e5eeef343db67305d1c1"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbe104f21a429b77eb5ac276bd5352fd8c0e1fbb580b4c772f77ee8c76825654"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db33deef2a5e1cf528ca51cc59dd764122a48a19a6c776283b223d147041153f"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9adf1307fa9d840d1acaa477eb04f9702032a483214c409fca9dc46f5f157fe3"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0afca3633c8e2b6c0a48ad0061180b641b3b404d68d7e6736aab301c8024c424"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2972241065b1c911bce3db808837ed10f4f6f8a8e15520a4242d291083605ab6"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f731d81cb939e757b0335b0090f18ca2e9ff8bcc8e6a1cf909245958949b6e11"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20c594eb56c19063ef5a57f89340e64c6550e169d6a29408a45130a8c3068adc"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:45d61a2b01bdf61581a2ee039503a08aa603dc74a6bbe6fb5d1ce3052f5370e5"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2107cec3699ca4d7bd41543dc1d475c97ae3a21ea9212238b5c2088fa8ee7722"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-musllinux_1_2_i686.whl", hash = "sha256:786de30723c71fc46b80a173c3313fc0dbe73c96bd9da8dd1212cbc2f84cdfb2"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a898953949e37c109dd242cfcf9841e065319995ebb7cdfd213b446094a942f"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-win32.whl", hash = "sha256:d12ab329474c46b96d962e2bdb92e3ad2144981fe41b89c7770f370646c0101f"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-win_amd64.whl", hash = "sha256:d09aecc9f5845586ba90911d815f9772c5a6dcf2e34be58c6017ecb124534ac4"},
 | 
			
		||||
    {file = "ruff-0.0.267-py3-none-win_arm64.whl", hash = "sha256:7df7eb5f8d791566ba97cc0b144981b9c080a5b861abaf4bb35a26c8a77b83e9"},
 | 
			
		||||
    {file = "ruff-0.0.267.tar.gz", hash = "sha256:632cec7bbaf3c06fcf0a72a1dd029b7d8b7f424ba95a574aaa135f5d20a00af7"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:30ddfe22de6ce4eb1260408f4480bbbce998f954dbf470228a21a9b2c45955e4"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a11bd0889e88d3342e7bc514554bb4461bf6cc30ec115821c2425cfaac0b1b6a"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a9b38bdb40a998cbc677db55b6225a6c4fadcf8819eb30695e1b8470942426b"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8b44a245b60512403a6a03a5b5212da274d33862225c5eed3bcf12037eb19bb"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b279fa55ea175ef953208a6d8bfbcdcffac1c39b38cdb8c2bfafe9222add70bb"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5028950f7af9b119d43d91b215d5044976e43b96a0d1458d193ef0dd3c587bf8"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4057eb539a1d88eb84e9f6a36e0a999e0f261ed850ae5d5817e68968e7b89ed9"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d586e69ab5cbf521a1910b733412a5735936f6a610d805b89d35b6647e2a66aa"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa17b13cd3f29fc57d06bf34c31f21d043735cc9a681203d634549b0e41047d1"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9ac13b11d9ad3001de9d637974ec5402a67cefdf9fffc3929ab44c2fcbb850a1"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:62a9578b48cfd292c64ea3d28681dc16b1aa7445b7a7709a2884510fc0822118"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0f9967f84da42d28e3d9d9354cc1575f96ed69e6e40a7d4b780a7a0418d9409"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d5a8de2fbaf91ea5699451a06f4074e7a312accfa774ad9327cde3e4fda2081"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-win32.whl", hash = "sha256:9e9db5ccb810742d621f93272e3cc23b5f277d8d00c4a79668835d26ccbe48dd"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-win_amd64.whl", hash = "sha256:f54facf286103006171a00ce20388d88ed1d6732db3b49c11feb9bf3d46f90e9"},
 | 
			
		||||
    {file = "ruff-0.0.265-py3-none-win_arm64.whl", hash = "sha256:c78470656e33d32ddc54e8482b1b0fc6de58f1195586731e5ff1405d74421499"},
 | 
			
		||||
    {file = "ruff-0.0.265.tar.gz", hash = "sha256:53c17f0dab19ddc22b254b087d1381b601b155acfa8feed514f0d6a413d0ab3a"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -3153,14 +3154,14 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "sentry-sdk"
 | 
			
		||||
version = "1.23.1"
 | 
			
		||||
version = "1.22.2"
 | 
			
		||||
description = "Python client for Sentry (https://sentry.io)"
 | 
			
		||||
category = "main"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
files = [
 | 
			
		||||
    {file = "sentry-sdk-1.23.1.tar.gz", hash = "sha256:0300fbe7a07b3865b3885929fb863a68ff01f59e3bcfb4e7953d0bf7fd19c67f"},
 | 
			
		||||
    {file = "sentry_sdk-1.23.1-py2.py3-none-any.whl", hash = "sha256:a884e2478e0b055776ea2b9234d5de9339b4bae0b3a5e74ae43d131db8ded27e"},
 | 
			
		||||
    {file = "sentry-sdk-1.22.2.tar.gz", hash = "sha256:5932c092c6e6035584eb74d77064e4bce3b7935dfc4a331349719a40db265840"},
 | 
			
		||||
    {file = "sentry_sdk-1.22.2-py2.py3-none-any.whl", hash = "sha256:cf89a5063ef84278d186aceaed6fb595bfe67d099298e537634a323664265669"},
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
@ -3177,11 +3178,10 @@ chalice = ["chalice (>=1.16.0)"]
 | 
			
		||||
django = ["django (>=1.8)"]
 | 
			
		||||
falcon = ["falcon (>=1.4)"]
 | 
			
		||||
fastapi = ["fastapi (>=0.79.0)"]
 | 
			
		||||
flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
 | 
			
		||||
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
 | 
			
		||||
grpcio = ["grpcio (>=1.21.1)"]
 | 
			
		||||
httpx = ["httpx (>=0.16.0)"]
 | 
			
		||||
huey = ["huey (>=2)"]
 | 
			
		||||
loguru = ["loguru (>=0.5)"]
 | 
			
		||||
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
 | 
			
		||||
pure-eval = ["asttokens", "executing", "pure-eval"]
 | 
			
		||||
pymongo = ["pymongo (>=3.1)"]
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,7 @@ FROM --platform=${BUILDPLATFORM} docker.io/node:20 as web-builder
 | 
			
		||||
COPY ./web /static/
 | 
			
		||||
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
WORKDIR /static
 | 
			
		||||
RUN npm ci --include=dev && npm run build-proxy
 | 
			
		||||
RUN cd /static && npm ci && npm run build-proxy
 | 
			
		||||
 | 
			
		||||
# Stage 2: Build
 | 
			
		||||
FROM docker.io/golang:1.20.4-bullseye AS builder
 | 
			
		||||
 | 
			
		||||
@ -113,7 +113,7 @@ filterwarnings = [
 | 
			
		||||
 | 
			
		||||
[tool.poetry]
 | 
			
		||||
name = "authentik"
 | 
			
		||||
version = "2023.5.6"
 | 
			
		||||
version = "2023.4.1"
 | 
			
		||||
description = ""
 | 
			
		||||
authors = ["authentik Team <hello@goauthentik.io>"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										80
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								schema.yml
									
									
									
									
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
openapi: 3.0.3
 | 
			
		||||
info:
 | 
			
		||||
  title: authentik
 | 
			
		||||
  version: 2023.5.6
 | 
			
		||||
  version: 2023.4.1
 | 
			
		||||
  description: Making authentication simple.
 | 
			
		||||
  contact:
 | 
			
		||||
    email: hello@goauthentik.io
 | 
			
		||||
@ -4783,38 +4783,6 @@ paths:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/GenericError'
 | 
			
		||||
          description: ''
 | 
			
		||||
  /core/users/{id}/impersonate/:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: core_users_impersonate_create
 | 
			
		||||
      description: Impersonate a user
 | 
			
		||||
      parameters:
 | 
			
		||||
      - in: path
 | 
			
		||||
        name: id
 | 
			
		||||
        schema:
 | 
			
		||||
          type: integer
 | 
			
		||||
        description: A unique integer value identifying this User.
 | 
			
		||||
        required: true
 | 
			
		||||
      tags:
 | 
			
		||||
      - core
 | 
			
		||||
      security:
 | 
			
		||||
      - authentik: []
 | 
			
		||||
      responses:
 | 
			
		||||
        '204':
 | 
			
		||||
          description: Successfully started impersonation
 | 
			
		||||
        '401':
 | 
			
		||||
          description: Access denied
 | 
			
		||||
        '400':
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/ValidationError'
 | 
			
		||||
          description: ''
 | 
			
		||||
        '403':
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/GenericError'
 | 
			
		||||
          description: ''
 | 
			
		||||
  /core/users/{id}/metrics/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: core_users_metrics_retrieve
 | 
			
		||||
@ -4994,29 +4962,6 @@ paths:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/GenericError'
 | 
			
		||||
          description: ''
 | 
			
		||||
  /core/users/impersonate_end/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: core_users_impersonate_end_retrieve
 | 
			
		||||
      description: End Impersonation a user
 | 
			
		||||
      tags:
 | 
			
		||||
      - core
 | 
			
		||||
      security:
 | 
			
		||||
      - authentik: []
 | 
			
		||||
      responses:
 | 
			
		||||
        '204':
 | 
			
		||||
          description: Successfully started impersonation
 | 
			
		||||
        '400':
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/ValidationError'
 | 
			
		||||
          description: ''
 | 
			
		||||
        '403':
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/GenericError'
 | 
			
		||||
          description: ''
 | 
			
		||||
  /core/users/me/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: core_users_me_retrieve
 | 
			
		||||
@ -39011,11 +38956,6 @@ components:
 | 
			
		||||
        shared_secret:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: Shared secret between clients and server to hash packets.
 | 
			
		||||
        outpost_set:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            type: string
 | 
			
		||||
          readOnly: true
 | 
			
		||||
      required:
 | 
			
		||||
      - assigned_application_name
 | 
			
		||||
      - assigned_application_slug
 | 
			
		||||
@ -39025,7 +38965,6 @@ components:
 | 
			
		||||
      - component
 | 
			
		||||
      - meta_model_name
 | 
			
		||||
      - name
 | 
			
		||||
      - outpost_set
 | 
			
		||||
      - pk
 | 
			
		||||
      - verbose_name
 | 
			
		||||
      - verbose_name_plural
 | 
			
		||||
@ -39885,11 +39824,11 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: Get object component so that we know how to edit the object
 | 
			
		||||
          readOnly: true
 | 
			
		||||
        assigned_backchannel_application_slug:
 | 
			
		||||
        assigned_application_slug:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: Internal application name, used in URLs.
 | 
			
		||||
          readOnly: true
 | 
			
		||||
        assigned_backchannel_application_name:
 | 
			
		||||
        assigned_application_name:
 | 
			
		||||
          type: string
 | 
			
		||||
          description: Application's display Name.
 | 
			
		||||
          readOnly: true
 | 
			
		||||
@ -39918,8 +39857,8 @@ components:
 | 
			
		||||
          format: uuid
 | 
			
		||||
          nullable: true
 | 
			
		||||
      required:
 | 
			
		||||
      - assigned_backchannel_application_name
 | 
			
		||||
      - assigned_backchannel_application_slug
 | 
			
		||||
      - assigned_application_name
 | 
			
		||||
      - assigned_application_slug
 | 
			
		||||
      - component
 | 
			
		||||
      - meta_model_name
 | 
			
		||||
      - name
 | 
			
		||||
@ -40548,6 +40487,12 @@ components:
 | 
			
		||||
      type: object
 | 
			
		||||
      description: Get system information.
 | 
			
		||||
      properties:
 | 
			
		||||
        env:
 | 
			
		||||
          type: object
 | 
			
		||||
          additionalProperties:
 | 
			
		||||
            type: string
 | 
			
		||||
          description: Get Environment
 | 
			
		||||
          readOnly: true
 | 
			
		||||
        http_headers:
 | 
			
		||||
          type: object
 | 
			
		||||
          additionalProperties:
 | 
			
		||||
@ -40601,6 +40546,7 @@ components:
 | 
			
		||||
          readOnly: true
 | 
			
		||||
      required:
 | 
			
		||||
      - embedded_outpost_host
 | 
			
		||||
      - env
 | 
			
		||||
      - http_headers
 | 
			
		||||
      - http_host
 | 
			
		||||
      - http_is_secure
 | 
			
		||||
@ -41025,6 +40971,7 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
      required:
 | 
			
		||||
      - avatar
 | 
			
		||||
      - groups
 | 
			
		||||
      - groups_obj
 | 
			
		||||
      - is_superuser
 | 
			
		||||
      - name
 | 
			
		||||
@ -41482,6 +41429,7 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
          minLength: 1
 | 
			
		||||
      required:
 | 
			
		||||
      - groups
 | 
			
		||||
      - name
 | 
			
		||||
      - username
 | 
			
		||||
    UserSAMLSourceConnection:
 | 
			
		||||
 | 
			
		||||
@ -243,7 +243,7 @@ class TestSourceOAuth1(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "ghcr.io/beryju/oauth1-test-server:v1.1",
 | 
			
		||||
            "image": "ghcr.io/beryju/oauth1-test-server:latest",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1408
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1408
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -16,24 +16,45 @@
 | 
			
		||||
        "background-image": "npx @squoosh/cli -d src/assets/images --resize '{\"enabled\":true,\"width\":2560,\"method\":\"lanczos3\",\"fitMethod\":\"contain\",\"premultiply\":true,\"linearRGB\":true}' --mozjpeg '{\"quality\":75,\"baseline\":false,\"arithmetic\":false,\"progressive\":true,\"optimize_coding\":true,\"smoothing\":0,\"color_space\":3,\"quant_table\":3,\"trellis_multipass\":false,\"trellis_opt_zero\":false,\"trellis_opt_table\":false,\"trellis_loops\":1,\"auto_subsample\":true,\"chroma_subsample\":2,\"separate_chroma_quality\":false,\"chroma_quality\":75}' src/assets/images/flow_background.jpg"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@babel/core": "^7.21.8",
 | 
			
		||||
        "@babel/plugin-proposal-decorators": "^7.21.0",
 | 
			
		||||
        "@babel/plugin-transform-runtime": "^7.21.4",
 | 
			
		||||
        "@babel/preset-env": "^7.21.5",
 | 
			
		||||
        "@babel/preset-typescript": "^7.21.5",
 | 
			
		||||
        "@codemirror/lang-html": "^6.4.3",
 | 
			
		||||
        "@codemirror/lang-javascript": "^6.1.8",
 | 
			
		||||
        "@codemirror/lang-javascript": "^6.1.7",
 | 
			
		||||
        "@codemirror/lang-python": "^6.1.2",
 | 
			
		||||
        "@codemirror/lang-xml": "^6.0.2",
 | 
			
		||||
        "@codemirror/legacy-modes": "^6.3.2",
 | 
			
		||||
        "@codemirror/theme-one-dark": "^6.1.2",
 | 
			
		||||
        "@formatjs/intl-listformat": "^7.2.2",
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^6.4.0",
 | 
			
		||||
        "@goauthentik/api": "^2023.5.3-1687462221",
 | 
			
		||||
        "@lingui/cli": "^4.1.2",
 | 
			
		||||
        "@lingui/core": "^4.1.2",
 | 
			
		||||
        "@lingui/detect-locale": "^4.1.2",
 | 
			
		||||
        "@lingui/format-po-gettext": "^4.1.2",
 | 
			
		||||
        "@lingui/macro": "^4.1.2",
 | 
			
		||||
        "@goauthentik/api": "^2023.4.1-1683802980",
 | 
			
		||||
        "@hcaptcha/types": "^1.0.3",
 | 
			
		||||
        "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
 | 
			
		||||
        "@lingui/cli": "^4.0.0",
 | 
			
		||||
        "@lingui/core": "^4.0.0",
 | 
			
		||||
        "@lingui/detect-locale": "^4.0.0",
 | 
			
		||||
        "@lingui/format-po-gettext": "^4.0.0",
 | 
			
		||||
        "@lingui/macro": "^4.0.0",
 | 
			
		||||
        "@patternfly/patternfly": "^4.224.2",
 | 
			
		||||
        "@sentry/browser": "^7.52.1",
 | 
			
		||||
        "@sentry/tracing": "^7.52.1",
 | 
			
		||||
        "@rollup/plugin-babel": "^6.0.3",
 | 
			
		||||
        "@rollup/plugin-commonjs": "^24.1.0",
 | 
			
		||||
        "@rollup/plugin-node-resolve": "^15.0.2",
 | 
			
		||||
        "@rollup/plugin-replace": "^5.0.2",
 | 
			
		||||
        "@rollup/plugin-typescript": "^11.1.0",
 | 
			
		||||
        "@sentry/browser": "^7.51.2",
 | 
			
		||||
        "@sentry/tracing": "^7.51.2",
 | 
			
		||||
        "@squoosh/cli": "^0.7.3",
 | 
			
		||||
        "@trivago/prettier-plugin-sort-imports": "^4.1.1",
 | 
			
		||||
        "@types/chart.js": "^2.9.37",
 | 
			
		||||
        "@types/codemirror": "5.60.7",
 | 
			
		||||
        "@types/grecaptcha": "^3.0.4",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^5.59.5",
 | 
			
		||||
        "@typescript-eslint/parser": "^5.59.5",
 | 
			
		||||
        "@webcomponents/webcomponentsjs": "^2.8.0",
 | 
			
		||||
        "babel-plugin-macros": "^3.1.0",
 | 
			
		||||
        "babel-plugin-tsconfig-paths": "^1.0.3",
 | 
			
		||||
        "base64-js": "^1.5.1",
 | 
			
		||||
        "chart.js": "^4.3.0",
 | 
			
		||||
        "chartjs-adapter-moment": "^1.0.1",
 | 
			
		||||
@ -41,49 +62,27 @@
 | 
			
		||||
        "construct-style-sheets-polyfill": "^3.1.0",
 | 
			
		||||
        "core-js": "^3.30.2",
 | 
			
		||||
        "country-flag-icons": "^1.5.7",
 | 
			
		||||
        "fuse.js": "^6.6.2",
 | 
			
		||||
        "lit": "^2.7.4",
 | 
			
		||||
        "mermaid": "^10.1.0",
 | 
			
		||||
        "rapidoc": "^9.3.4",
 | 
			
		||||
        "webcomponent-qr-code": "^1.1.1",
 | 
			
		||||
        "yaml": "^2.2.2"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@babel/core": "^7.21.8",
 | 
			
		||||
        "@babel/plugin-proposal-decorators": "^7.21.0",
 | 
			
		||||
        "@babel/plugin-transform-runtime": "^7.21.4",
 | 
			
		||||
        "@babel/preset-env": "^7.21.5",
 | 
			
		||||
        "@babel/preset-typescript": "^7.21.5",
 | 
			
		||||
        "@hcaptcha/types": "^1.0.3",
 | 
			
		||||
        "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
 | 
			
		||||
        "@rollup/plugin-babel": "^6.0.3",
 | 
			
		||||
        "@rollup/plugin-commonjs": "^25.0.0",
 | 
			
		||||
        "@rollup/plugin-node-resolve": "^15.0.2",
 | 
			
		||||
        "@rollup/plugin-replace": "^5.0.2",
 | 
			
		||||
        "@rollup/plugin-typescript": "^11.1.1",
 | 
			
		||||
        "@squoosh/cli": "^0.7.3",
 | 
			
		||||
        "@trivago/prettier-plugin-sort-imports": "^4.1.1",
 | 
			
		||||
        "@types/chart.js": "^2.9.37",
 | 
			
		||||
        "@types/codemirror": "5.60.7",
 | 
			
		||||
        "@types/grecaptcha": "^3.0.4",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^5.59.6",
 | 
			
		||||
        "@typescript-eslint/parser": "^5.59.6",
 | 
			
		||||
        "babel-plugin-macros": "^3.1.0",
 | 
			
		||||
        "babel-plugin-tsconfig-paths": "^1.0.3",
 | 
			
		||||
        "eslint": "^8.40.0",
 | 
			
		||||
        "eslint-config-google": "^0.14.0",
 | 
			
		||||
        "eslint-plugin-custom-elements": "0.0.8",
 | 
			
		||||
        "eslint-plugin-lit": "^1.8.3",
 | 
			
		||||
        "fuse.js": "^6.6.2",
 | 
			
		||||
        "lit": "^2.7.4",
 | 
			
		||||
        "mermaid": "^10.1.0",
 | 
			
		||||
        "moment": "^2.29.4",
 | 
			
		||||
        "prettier": "^2.8.8",
 | 
			
		||||
        "pyright": "^1.1.308",
 | 
			
		||||
        "pyright": "^1.1.307",
 | 
			
		||||
        "rapidoc": "^9.3.4",
 | 
			
		||||
        "rollup": "^2.79.1",
 | 
			
		||||
        "rollup-plugin-copy": "^3.4.0",
 | 
			
		||||
        "rollup-plugin-cssimport": "^1.0.3",
 | 
			
		||||
        "rollup-plugin-minify-html-literals": "^1.2.6",
 | 
			
		||||
        "rollup-plugin-terser": "^7.0.2",
 | 
			
		||||
        "ts-lit-plugin": "^1.2.1",
 | 
			
		||||
        "tslib": "^2.5.1",
 | 
			
		||||
        "tslib": "^2.5.0",
 | 
			
		||||
        "turnstile-types": "^1.1.2",
 | 
			
		||||
        "typescript": "^5.0.4"
 | 
			
		||||
        "typescript": "^5.0.4",
 | 
			
		||||
        "webcomponent-qr-code": "^1.1.1",
 | 
			
		||||
        "yaml": "^2.2.2"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
 | 
			
		||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { AdminApi, CoreApi, SessionUser, Version } from "@goauthentik/api";
 | 
			
		||||
import { AdminApi, SessionUser, Version } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
autoDetectLanguage();
 | 
			
		||||
 | 
			
		||||
@ -175,11 +175,10 @@ export class AdminInterface extends Interface {
 | 
			
		||||
            ${this.user?.original
 | 
			
		||||
                ? html`<ak-sidebar-item
 | 
			
		||||
                      ?highlight=${true}
 | 
			
		||||
                      @click=${() => {
 | 
			
		||||
                          new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
 | 
			
		||||
                              window.location.reload();
 | 
			
		||||
                          });
 | 
			
		||||
                      }}
 | 
			
		||||
                      ?isAbsoluteLink=${true}
 | 
			
		||||
                      path=${`/-/impersonation/end/?back=${encodeURIComponent(
 | 
			
		||||
                          `${window.location.pathname}#${window.location.hash}`,
 | 
			
		||||
                      )}`}
 | 
			
		||||
                  >
 | 
			
		||||
                      <span slot="label"
 | 
			
		||||
                          >${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span
 | 
			
		||||
 | 
			
		||||
@ -115,8 +115,9 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInlineForm(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
 | 
			
		||||
                <ak-search-select
 | 
			
		||||
                    .fetchObjects=${async (query?: string): Promise<User[]> => {
 | 
			
		||||
                        const args: CoreUsersListRequest = {
 | 
			
		||||
@ -143,6 +144,7 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
 | 
			
		||||
                >
 | 
			
		||||
                </ak-search-select>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}`;
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,12 +21,9 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInlineForm(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal
 | 
			
		||||
                label=${t`Common Name`}
 | 
			
		||||
                name="commonName"
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
            >
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}>
 | 
			
		||||
                <input type="text" class="pf-c-form-control" required />
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            <ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName">
 | 
			
		||||
@ -41,6 +38,7 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
            >
 | 
			
		||||
                <input class="pf-c-form-control" type="number" value="365" />
 | 
			
		||||
            </ak-form-element-horizontal>`;
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,13 +87,15 @@ export class FlowImportForm extends Form<Flow> {
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInlineForm(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${t`Flow`} name="flow">
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${t`Flow`} name="flow">
 | 
			
		||||
                <input type="file" value="" class="pf-c-form-control" />
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}`;
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -46,8 +46,9 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInlineForm(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${t`Groups to add`} name="groups">
 | 
			
		||||
                <div class="pf-c-input-group">
 | 
			
		||||
                    <ak-user-group-select-table
 | 
			
		||||
                        .confirm=${(items: Group[]) => {
 | 
			
		||||
@ -78,7 +79,8 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
 | 
			
		||||
                        </ak-chip-group>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
        </ak-form-element-horizontal>`;
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -191,12 +191,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
 | 
			
		||||
                        const selected = Array.from(this.instance?.providers || []).some((sp) => {
 | 
			
		||||
                            return sp == provider.pk;
 | 
			
		||||
                        });
 | 
			
		||||
                        let appName = provider.assignedApplicationName;
 | 
			
		||||
                        if (provider.assignedBackchannelApplicationName) {
 | 
			
		||||
                            appName = provider.assignedBackchannelApplicationName;
 | 
			
		||||
                        }
 | 
			
		||||
                        return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
 | 
			
		||||
                            ${appName} (${provider.name})
 | 
			
		||||
                            ${provider.assignedApplicationName} (${provider.name})
 | 
			
		||||
                        </option>`;
 | 
			
		||||
                    })}
 | 
			
		||||
                </select>
 | 
			
		||||
 | 
			
		||||
@ -116,8 +116,9 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInlineForm(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
 | 
			
		||||
                <ak-search-select
 | 
			
		||||
                    .fetchObjects=${async (query?: string): Promise<User[]> => {
 | 
			
		||||
                        const args: CoreUsersListRequest = {
 | 
			
		||||
@ -154,6 +155,7 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
 | 
			
		||||
                    ${t`Set custom attributes using YAML or JSON.`}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}`;
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -119,8 +119,9 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderInlineForm(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
 | 
			
		||||
                <ak-search-select
 | 
			
		||||
                    .fetchObjects=${async (query?: string): Promise<User[]> => {
 | 
			
		||||
                        const args: CoreUsersListRequest = {
 | 
			
		||||
@ -155,6 +156,7 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
 | 
			
		||||
                </ak-codemirror>
 | 
			
		||||
                <p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}`;
 | 
			
		||||
            ${this.result ? this.renderResult() : html``}
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user