Compare commits
	
		
			159 Commits
		
	
	
		
			version/20
			...
			enterprise
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 60000812fd | |||
| 929e70669a | |||
| 36114284bf | |||
| a65ea0de94 | |||
| 2056b0cbee | |||
| af85ddf60b | |||
| e8e42261e3 | |||
| 9fda4e91ad | |||
| a11f1258e1 | |||
| a97578ac62 | |||
| 41aa36d06f | |||
| 62fc4c56e4 | |||
| 4514412010 | |||
| 463efac469 | |||
| f4508659cf | |||
| 336f6f0dc2 | |||
| c19a887356 | |||
| 09931bcbc2 | |||
| 7a4293bf17 | |||
| 6e569acd84 | |||
| 02c69d767f | |||
| 1863a9a12b | |||
| b981bc5ba1 | |||
| 5da02971eb | |||
| 1f49ee77df | |||
| baf8f18d54 | |||
| 5445b1235a | |||
| 2893a54ffb | |||
| 94eff50306 | |||
| 0befc26507 | |||
| 629d5df763 | |||
| 3098313981 | |||
| c0a370bb2b | |||
| a19d915d2b | |||
| 9a0dc50174 | |||
| ac0a708f92 | |||
| 0ffaf0393e | |||
| 9bb3aa0374 | |||
| f6a32dc6e5 | |||
| af83fc7245 | |||
| 84de15568a | |||
| 29f8a82b49 | |||
| cd05c0ec19 | |||
| c19a1b373a | |||
| 31b9cbfb85 | |||
| c0fe0dab61 | |||
| 1bd42345b9 | |||
| 90e7545d57 | |||
| 78d42c391d | |||
| 2ad831adb0 | |||
| 5eaa94917b | |||
| 6c0d462410 | |||
| 9dc2c26ba9 | |||
| 774a84f9e6 | |||
| 56015d883b | |||
| 9d15fa4a57 | |||
| bb7338f5c1 | |||
| f949141d03 | |||
| 646d133c30 | |||
| 3ee3adc509 | |||
| 1b4fee2bac | |||
| 10c358401d | |||
| 9dddbd2f0c | |||
| 078d643c20 | |||
| 733b7cf139 | |||
| f83fab214b | |||
| 9ce460a0ac | |||
| e69a380a39 | |||
| 2d89f42c68 | |||
| 3d4d167542 | |||
| ee8d3c5146 | |||
| 0406b0d95a | |||
| 44d49bb14c | |||
| afb1686be7 | |||
| 6b1802697d | |||
| 943fd6b78b | |||
| ed33d314cd | |||
| d343ccc539 | |||
| 31e8fb7c8c | |||
| 23faa0b839 | |||
| 3cbfd836ac | |||
| 10ab6e4327 | |||
| 561d2220bc | |||
| e6c47db9f8 | |||
| 5f5171c472 | |||
| bdf4236973 | |||
| a61a41d7d0 | |||
| c7532d35f2 | |||
| 27baedfea4 | |||
| e3011eab9a | |||
| 9635dd98f3 | |||
| bd0d7edbc4 | |||
| 9b05418306 | |||
| d4e15f0f39 | |||
| ec9c2266eb | |||
| 5ebd280087 | |||
| 1cc8d80600 | |||
| 3b70cd735e | |||
| 42766e13da | |||
| 8938fa5a7e | |||
| 4c8f610cdb | |||
| 8690200cd8 | |||
| 91145b7929 | |||
| d255e53756 | |||
| d51e6a5551 | |||
| 5433839ea0 | |||
| 863a7e6095 | |||
| 50db80428c | |||
| ffd5234396 | |||
| 95890638a5 | |||
| f7d2a68b1d | |||
| 83ecb64f33 | |||
| 40b0f7df8d | |||
| ee6fcdfbd8 | |||
| 94623615a6 | |||
| aa4f817856 | |||
| c3aefd55a2 | |||
| 1298cdc338 | |||
| 3eaaa35a4c | |||
| d17f781d11 | |||
| c82b79f10f | |||
| 0aa7be6e2c | |||
| 9811ec57df | |||
| 393e5f236c | |||
| 59ae9c6148 | |||
| fd8e20bdeb | |||
| 737aced000 | |||
| dc3559c7e9 | |||
| 02bd699917 | |||
| 5fccbd7c04 | |||
| 6fc92bd50c | |||
| 687f6d683a | |||
| 4a8329649c | |||
| 0c296efede | |||
| 112520fd88 | |||
| ee648269f7 | |||
| 15be3f2461 | |||
| ef9557c578 | |||
| 48700c0e9c | |||
| 18a48030a8 | |||
| 640d0a4a95 | |||
| 6b8782556c | |||
| 7f6f3b6602 | |||
| 3367ac0e08 | |||
| d5ea0ffdc6 | |||
| 93f1638b39 | |||
| 37525175fa | |||
| 0db1e52f90 | |||
| 3e8620b686 | |||
| 6687ffc6d2 | |||
| e265ee253b | |||
| 7763a3673c | |||
| d99005e130 | |||
| c61f96e770 | |||
| 83622dd934 | |||
| 2eebd0eaa1 | |||
| b61d918c5c | |||
| 076a4f4772 | |||
| b3872b35f8 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2024.12.1
 | 
					current_version = 2024.12.2
 | 
				
			||||||
tag = True
 | 
					tag = True
 | 
				
			||||||
commit = True
 | 
					commit = True
 | 
				
			||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
 | 
					parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -134,7 +134,7 @@ jobs:
 | 
				
			|||||||
      - name: Setup authentik env
 | 
					      - name: Setup authentik env
 | 
				
			||||||
        uses: ./.github/actions/setup
 | 
					        uses: ./.github/actions/setup
 | 
				
			||||||
      - name: Create k8s Kind Cluster
 | 
					      - name: Create k8s Kind Cluster
 | 
				
			||||||
        uses: helm/kind-action@v1.11.0
 | 
					        uses: helm/kind-action@v1.12.0
 | 
				
			||||||
      - name: run integration
 | 
					      - name: run integration
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          poetry run coverage run manage.py test tests/integration
 | 
					          poetry run coverage run manage.py test tests/integration
 | 
				
			||||||
@ -168,6 +168,8 @@ jobs:
 | 
				
			|||||||
            glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
 | 
					            glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
 | 
				
			||||||
          - name: ldap
 | 
					          - name: ldap
 | 
				
			||||||
            glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
 | 
					            glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
 | 
				
			||||||
 | 
					          - name: rac
 | 
				
			||||||
 | 
					            glob: tests/e2e/test_provider_rac*
 | 
				
			||||||
          - name: radius
 | 
					          - name: radius
 | 
				
			||||||
            glob: tests/e2e/test_provider_radius*
 | 
					            glob: tests/e2e/test_provider_radius*
 | 
				
			||||||
          - name: scim
 | 
					          - name: scim
 | 
				
			||||||
@ -243,7 +245,7 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
        uses: docker/setup-qemu-action@v3.2.0
 | 
					        uses: docker/setup-qemu-action@v3.3.0
 | 
				
			||||||
      - name: Set up Docker Buildx
 | 
					      - name: Set up Docker Buildx
 | 
				
			||||||
        uses: docker/setup-buildx-action@v3
 | 
					        uses: docker/setup-buildx-action@v3
 | 
				
			||||||
      - name: prepare variables
 | 
					      - name: prepare variables
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -82,7 +82,7 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          ref: ${{ github.event.pull_request.head.sha }}
 | 
					          ref: ${{ github.event.pull_request.head.sha }}
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
        uses: docker/setup-qemu-action@v3.2.0
 | 
					        uses: docker/setup-qemu-action@v3.3.0
 | 
				
			||||||
      - name: Set up Docker Buildx
 | 
					      - name: Set up Docker Buildx
 | 
				
			||||||
        uses: docker/setup-buildx-action@v3
 | 
					        uses: docker/setup-buildx-action@v3
 | 
				
			||||||
      - name: prepare variables
 | 
					      - name: prepare variables
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@ -2,7 +2,7 @@ name: "CodeQL"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    branches: [main, next, version*]
 | 
					    branches: [main, "*", next, version*]
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    branches: [main]
 | 
					    branches: [main]
 | 
				
			||||||
  schedule:
 | 
					  schedule:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -17,7 +17,7 @@ jobs:
 | 
				
			|||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
        uses: docker/setup-qemu-action@v3.2.0
 | 
					        uses: docker/setup-qemu-action@v3.3.0
 | 
				
			||||||
      - name: Set up Docker Buildx
 | 
					      - name: Set up Docker Buildx
 | 
				
			||||||
        uses: docker/setup-buildx-action@v3
 | 
					        uses: docker/setup-buildx-action@v3
 | 
				
			||||||
      - name: prepare variables
 | 
					      - name: prepare variables
 | 
				
			||||||
@ -83,7 +83,7 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          go-version-file: "go.mod"
 | 
					          go-version-file: "go.mod"
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
        uses: docker/setup-qemu-action@v3.2.0
 | 
					        uses: docker/setup-qemu-action@v3.3.0
 | 
				
			||||||
      - name: Set up Docker Buildx
 | 
					      - name: Set up Docker Buildx
 | 
				
			||||||
        uses: docker/setup-buildx-action@v3
 | 
					        uses: docker/setup-buildx-action@v3
 | 
				
			||||||
      - name: prepare variables
 | 
					      - name: prepare variables
 | 
				
			||||||
@ -188,8 +188,8 @@ jobs:
 | 
				
			|||||||
          aws-region: ${{ env.AWS_REGION }}
 | 
					          aws-region: ${{ env.AWS_REGION }}
 | 
				
			||||||
      - name: Upload template
 | 
					      - name: Upload template
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
 | 
					          aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
 | 
				
			||||||
          aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
 | 
					          aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
 | 
				
			||||||
  test-release:
 | 
					  test-release:
 | 
				
			||||||
    needs:
 | 
					    needs:
 | 
				
			||||||
      - build-server
 | 
					      - build-server
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = "2024.12.1"
 | 
					__version__ = "2024.12.2"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -16,5 +16,5 @@ def get_full_version() -> str:
 | 
				
			|||||||
    """Get full version, with build hash appended"""
 | 
					    """Get full version, with build hash appended"""
 | 
				
			||||||
    version = __version__
 | 
					    version = __version__
 | 
				
			||||||
    if (build_hash := get_build_hash()) != "":
 | 
					    if (build_hash := get_build_hash()) != "":
 | 
				
			||||||
        version += "." + build_hash
 | 
					        return f"{version}+{build_hash}"
 | 
				
			||||||
    return version
 | 
					    return version
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,9 @@ from sys import version as python_version
 | 
				
			|||||||
from typing import TypedDict
 | 
					from typing import TypedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from cryptography.hazmat.backends.openssl.backend import backend
 | 
					from cryptography.hazmat.backends.openssl.backend import backend
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
 | 
					from django.views.debug import SafeExceptionReporterFilter
 | 
				
			||||||
from drf_spectacular.utils import extend_schema
 | 
					from drf_spectacular.utils import extend_schema
 | 
				
			||||||
from rest_framework.fields import SerializerMethodField
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
@ -52,10 +54,16 @@ class SystemInfoSerializer(PassiveSerializer):
 | 
				
			|||||||
    def get_http_headers(self, request: Request) -> dict[str, str]:
 | 
					    def get_http_headers(self, request: Request) -> dict[str, str]:
 | 
				
			||||||
        """Get HTTP Request headers"""
 | 
					        """Get HTTP Request headers"""
 | 
				
			||||||
        headers = {}
 | 
					        headers = {}
 | 
				
			||||||
 | 
					        raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
 | 
				
			||||||
        for key, value in request.META.items():
 | 
					        for key, value in request.META.items():
 | 
				
			||||||
            if not isinstance(value, str):
 | 
					            if not isinstance(value, str):
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            headers[key] = value
 | 
					            actual_value = value
 | 
				
			||||||
 | 
					            if raw_session in actual_value:
 | 
				
			||||||
 | 
					                actual_value = actual_value.replace(
 | 
				
			||||||
 | 
					                    raw_session, SafeExceptionReporterFilter.cleansed_substitute
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            headers[key] = actual_value
 | 
				
			||||||
        return headers
 | 
					        return headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_http_host(self, request: Request) -> str:
 | 
					    def get_http_host(self, request: Request) -> str:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,16 @@
 | 
				
			|||||||
"""authentik administration overview"""
 | 
					"""authentik administration overview"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from socket import gethostname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from drf_spectacular.utils import extend_schema, inline_serializer
 | 
					from drf_spectacular.utils import extend_schema, inline_serializer
 | 
				
			||||||
from rest_framework.fields import IntegerField
 | 
					from packaging.version import parse
 | 
				
			||||||
 | 
					from rest_framework.fields import BooleanField, CharField
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik import get_full_version
 | 
				
			||||||
from authentik.rbac.permissions import HasPermission
 | 
					from authentik.rbac.permissions import HasPermission
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -16,11 +20,38 @@ class WorkerView(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    permission_classes = [HasPermission("authentik_rbac.view_system_info")]
 | 
					    permission_classes = [HasPermission("authentik_rbac.view_system_info")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        responses=inline_serializer(
 | 
				
			||||||
 | 
					            "Worker",
 | 
				
			||||||
 | 
					            fields={
 | 
				
			||||||
 | 
					                "worker_id": CharField(),
 | 
				
			||||||
 | 
					                "version": CharField(),
 | 
				
			||||||
 | 
					                "version_matching": BooleanField(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            many=True,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def get(self, request: Request) -> Response:
 | 
					    def get(self, request: Request) -> Response:
 | 
				
			||||||
        """Get currently connected worker count."""
 | 
					        """Get currently connected worker count."""
 | 
				
			||||||
        count = len(CELERY_APP.control.ping(timeout=0.5))
 | 
					        raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
 | 
				
			||||||
 | 
					        our_version = parse(get_full_version())
 | 
				
			||||||
 | 
					        response = []
 | 
				
			||||||
 | 
					        for worker in raw:
 | 
				
			||||||
 | 
					            key = list(worker.keys())[0]
 | 
				
			||||||
 | 
					            version = worker[key].get("version")
 | 
				
			||||||
 | 
					            version_matching = False
 | 
				
			||||||
 | 
					            if version:
 | 
				
			||||||
 | 
					                version_matching = parse(version) == our_version
 | 
				
			||||||
 | 
					            response.append(
 | 
				
			||||||
 | 
					                {"worker_id": key, "version": version, "version_matching": version_matching}
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        # In debug we run with `task_always_eager`, so tasks are ran on the main process
 | 
					        # In debug we run with `task_always_eager`, so tasks are ran on the main process
 | 
				
			||||||
        if settings.DEBUG:  # pragma: no cover
 | 
					        if settings.DEBUG:  # pragma: no cover
 | 
				
			||||||
            count += 1
 | 
					            response.append(
 | 
				
			||||||
        return Response({"count": count})
 | 
					                {
 | 
				
			||||||
 | 
					                    "worker_id": f"authentik-debug@{gethostname()}",
 | 
				
			||||||
 | 
					                    "version": get_full_version(),
 | 
				
			||||||
 | 
					                    "version_matching": True,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        return Response(response)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,10 @@
 | 
				
			|||||||
"""authentik admin app config"""
 | 
					"""authentik admin app config"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from prometheus_client import Gauge, Info
 | 
					from prometheus_client import Info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.blueprints.apps import ManagedAppConfig
 | 
					from authentik.blueprints.apps import ManagedAppConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
 | 
					PROM_INFO = Info("authentik_version", "Currently running authentik version")
 | 
				
			||||||
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthentikAdminConfig(ManagedAppConfig):
 | 
					class AuthentikAdminConfig(ManagedAppConfig):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,35 @@
 | 
				
			|||||||
"""admin signals"""
 | 
					"""admin signals"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
 | 
					from packaging.version import parse
 | 
				
			||||||
 | 
					from prometheus_client import Gauge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.apps import GAUGE_WORKERS
 | 
					from authentik import get_full_version
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
from authentik.root.monitoring import monitoring_set
 | 
					from authentik.root.monitoring import monitoring_set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GAUGE_WORKERS = Gauge(
 | 
				
			||||||
 | 
					    "authentik_admin_workers",
 | 
				
			||||||
 | 
					    "Currently connected workers, their versions and if they are the same version as authentik",
 | 
				
			||||||
 | 
					    ["version", "version_matched"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_version = parse(get_full_version())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(monitoring_set)
 | 
					@receiver(monitoring_set)
 | 
				
			||||||
def monitoring_set_workers(sender, **kwargs):
 | 
					def monitoring_set_workers(sender, **kwargs):
 | 
				
			||||||
    """Set worker gauge"""
 | 
					    """Set worker gauge"""
 | 
				
			||||||
    count = len(CELERY_APP.control.ping(timeout=0.5))
 | 
					    raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
 | 
				
			||||||
    GAUGE_WORKERS.set(count)
 | 
					    worker_version_count = {}
 | 
				
			||||||
 | 
					    for worker in raw:
 | 
				
			||||||
 | 
					        key = list(worker.keys())[0]
 | 
				
			||||||
 | 
					        version = worker[key].get("version")
 | 
				
			||||||
 | 
					        version_matching = False
 | 
				
			||||||
 | 
					        if version:
 | 
				
			||||||
 | 
					            version_matching = parse(version) == _version
 | 
				
			||||||
 | 
					        worker_version_count.setdefault(version, {"count": 0, "matching": version_matching})
 | 
				
			||||||
 | 
					        worker_version_count[version]["count"] += 1
 | 
				
			||||||
 | 
					    for version, stats in worker_version_count.items():
 | 
				
			||||||
 | 
					        GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"])
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ class TestAdminAPI(TestCase):
 | 
				
			|||||||
        response = self.client.get(reverse("authentik_api:admin_workers"))
 | 
					        response = self.client.get(reverse("authentik_api:admin_workers"))
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        body = loads(response.content)
 | 
					        body = loads(response.content)
 | 
				
			||||||
        self.assertEqual(body["count"], 0)
 | 
					        self.assertEqual(len(body), 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_metrics(self):
 | 
					    def test_metrics(self):
 | 
				
			||||||
        """Test metrics API"""
 | 
					        """Test metrics API"""
 | 
				
			||||||
 | 
				
			|||||||
@ -1,67 +0,0 @@
 | 
				
			|||||||
"""API Authorization"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
from django.db.models import Model
 | 
					 | 
				
			||||||
from django.db.models.query import QuerySet
 | 
					 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework.authentication import get_authorization_header
 | 
					 | 
				
			||||||
from rest_framework.filters import BaseFilterBackend
 | 
					 | 
				
			||||||
from rest_framework.permissions import BasePermission
 | 
					 | 
				
			||||||
from rest_framework.request import Request
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from authentik.api.authentication import validate_auth
 | 
					 | 
				
			||||||
from authentik.rbac.filters import ObjectFilter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class OwnerFilter(BaseFilterBackend):
 | 
					 | 
				
			||||||
    """Filter objects by their owner"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    owner_key = "user"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
 | 
					 | 
				
			||||||
        if request.user.is_superuser:
 | 
					 | 
				
			||||||
            return queryset
 | 
					 | 
				
			||||||
        return queryset.filter(**{self.owner_key: request.user})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SecretKeyFilter(DjangoFilterBackend):
 | 
					 | 
				
			||||||
    """Allow access to all objects when authenticated with secret key as token.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Replaces both DjangoFilterBackend and ObjectFilter"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
 | 
					 | 
				
			||||||
        auth_header = get_authorization_header(request)
 | 
					 | 
				
			||||||
        token = validate_auth(auth_header)
 | 
					 | 
				
			||||||
        if token and token == settings.SECRET_KEY:
 | 
					 | 
				
			||||||
            return queryset
 | 
					 | 
				
			||||||
        queryset = ObjectFilter().filter_queryset(request, queryset, view)
 | 
					 | 
				
			||||||
        return super().filter_queryset(request, queryset, view)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class OwnerPermissions(BasePermission):
 | 
					 | 
				
			||||||
    """Authorize requests by an object's owner matching the requesting user"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    owner_key = "user"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def has_permission(self, request: Request, view) -> bool:
 | 
					 | 
				
			||||||
        """If the user is authenticated, we allow all requests here. For listing, the
 | 
					 | 
				
			||||||
        object-level permissions are done by the filter backend"""
 | 
					 | 
				
			||||||
        return request.user.is_authenticated
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def has_object_permission(self, request: Request, view, obj: Model) -> bool:
 | 
					 | 
				
			||||||
        """Check if the object's owner matches the currently logged in user"""
 | 
					 | 
				
			||||||
        if not hasattr(obj, self.owner_key):
 | 
					 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
        owner = getattr(obj, self.owner_key)
 | 
					 | 
				
			||||||
        if owner != request.user:
 | 
					 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
        return True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class OwnerSuperuserPermissions(OwnerPermissions):
 | 
					 | 
				
			||||||
    """Similar to OwnerPermissions, except always allow access for superusers"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def has_object_permission(self, request: Request, view, obj: Model) -> bool:
 | 
					 | 
				
			||||||
        if request.user.is_superuser:
 | 
					 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        return super().has_object_permission(request, view, obj)
 | 
					 | 
				
			||||||
							
								
								
									
										68
									
								
								authentik/blueprints/management/commands/blueprint_shell.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								authentik/blueprints/management/commands/blueprint_shell.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					"""Test and debug Blueprints"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import atexit
 | 
				
			||||||
 | 
					import readline
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from pprint import pformat
 | 
				
			||||||
 | 
					from sys import exit as sysexit
 | 
				
			||||||
 | 
					from textwrap import indent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.management.base import BaseCommand, no_translations
 | 
				
			||||||
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					from yaml import load
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.blueprints.v1.common import BlueprintLoader, EntryInvalidError
 | 
				
			||||||
 | 
					from authentik.core.management.commands.shell import get_banner_text
 | 
				
			||||||
 | 
					from authentik.lib.utils.errors import exception_to_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    """Test and debug Blueprints"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lines = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs) -> None:
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        histfolder = Path("~").expanduser() / Path(".local/share/authentik")
 | 
				
			||||||
 | 
					        histfolder.mkdir(parents=True, exist_ok=True)
 | 
				
			||||||
 | 
					        histfile = histfolder / Path("blueprint_shell_history")
 | 
				
			||||||
 | 
					        readline.parse_and_bind("tab: complete")
 | 
				
			||||||
 | 
					        readline.parse_and_bind("set editing-mode vi")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            readline.read_history_file(str(histfile))
 | 
				
			||||||
 | 
					        except FileNotFoundError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        atexit.register(readline.write_history_file, str(histfile))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @no_translations
 | 
				
			||||||
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
 | 
					        """Interactively debug blueprint files"""
 | 
				
			||||||
 | 
					        self.stdout.write(get_banner_text("Blueprint shell"))
 | 
				
			||||||
 | 
					        self.stdout.write("Type '.eval' to evaluate previously entered statement(s).")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def do_eval():
 | 
				
			||||||
 | 
					            yaml_input = "\n".join([line for line in self.lines if line])
 | 
				
			||||||
 | 
					            data = load(yaml_input, BlueprintLoader)
 | 
				
			||||||
 | 
					            self.stdout.write(pformat(data))
 | 
				
			||||||
 | 
					            self.lines = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                line = input("> ")
 | 
				
			||||||
 | 
					                if line == ".eval":
 | 
				
			||||||
 | 
					                    do_eval()
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    self.lines.append(line)
 | 
				
			||||||
 | 
					            except EntryInvalidError as exc:
 | 
				
			||||||
 | 
					                self.stdout.write("Failed to evaluate expression:")
 | 
				
			||||||
 | 
					                self.stdout.write(indent(exception_to_string(exc), prefix="  "))
 | 
				
			||||||
 | 
					            except EOFError:
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					            except KeyboardInterrupt:
 | 
				
			||||||
 | 
					                self.stdout.write()
 | 
				
			||||||
 | 
					                sysexit(0)
 | 
				
			||||||
 | 
					        self.stdout.write()
 | 
				
			||||||
@ -126,7 +126,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
        def_name_perm = f"model_{model_path}_permissions"
 | 
					        def_name_perm = f"model_{model_path}_permissions"
 | 
				
			||||||
        def_path_perm = f"#/$defs/{def_name_perm}"
 | 
					        def_path_perm = f"#/$defs/{def_name_perm}"
 | 
				
			||||||
        self.schema["$defs"][def_name_perm] = self.model_permissions(model)
 | 
					        self.schema["$defs"][def_name_perm] = self.model_permissions(model)
 | 
				
			||||||
        return {
 | 
					        template = {
 | 
				
			||||||
            "type": "object",
 | 
					            "type": "object",
 | 
				
			||||||
            "required": ["model", "identifiers"],
 | 
					            "required": ["model", "identifiers"],
 | 
				
			||||||
            "properties": {
 | 
					            "properties": {
 | 
				
			||||||
@ -143,6 +143,11 @@ class Command(BaseCommand):
 | 
				
			|||||||
                "identifiers": {"$ref": def_path},
 | 
					                "identifiers": {"$ref": def_path},
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        # Meta models don't require identifiers, as there's no matching database model to find
 | 
				
			||||||
 | 
					        if issubclass(model, BaseMetaModel):
 | 
				
			||||||
 | 
					            del template["properties"]["identifiers"]
 | 
				
			||||||
 | 
					            template["required"].remove("identifiers")
 | 
				
			||||||
 | 
					        return template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def field_to_jsonschema(self, field: Field) -> dict:
 | 
					    def field_to_jsonschema(self, field: Field) -> dict:
 | 
				
			||||||
        """Convert a single field to json schema"""
 | 
					        """Convert a single field to json schema"""
 | 
				
			||||||
 | 
				
			|||||||
@ -202,6 +202,9 @@ class Blueprint:
 | 
				
			|||||||
class YAMLTag:
 | 
					class YAMLTag:
 | 
				
			||||||
    """Base class for all YAML Tags"""
 | 
					    """Base class for all YAML Tags"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        return str(self.resolve(BlueprintEntry(""), Blueprint()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
 | 
					    def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
 | 
				
			||||||
        """Implement yaml tag logic"""
 | 
					        """Implement yaml tag logic"""
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
				
			|||||||
@ -14,10 +14,10 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.validators import UniqueValidator
 | 
					from rest_framework.validators import UniqueValidator
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import SecretKeyFilter
 | 
					 | 
				
			||||||
from authentik.brands.models import Brand
 | 
					from authentik.brands.models import Brand
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
 | 
					from authentik.core.api.utils import ModelSerializer, PassiveSerializer
 | 
				
			||||||
 | 
					from authentik.rbac.filters import SecretKeyFilter
 | 
				
			||||||
from authentik.tenants.utils import get_current_tenant
 | 
					from authentik.tenants.utils import get_current_tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,16 @@
 | 
				
			|||||||
"""Application Roles API Viewset"""
 | 
					"""Application Roles API Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from rest_framework.exceptions import ValidationError
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.core.models import (
 | 
					from authentik.core.models import (
 | 
				
			||||||
    Application,
 | 
					    Application,
 | 
				
			||||||
    ApplicationEntitlement,
 | 
					    ApplicationEntitlement,
 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,7 +19,10 @@ class ApplicationEntitlementSerializer(ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def validate_app(self, app: Application) -> Application:
 | 
					    def validate_app(self, app: Application) -> Application:
 | 
				
			||||||
        """Ensure user has permission to view"""
 | 
					        """Ensure user has permission to view"""
 | 
				
			||||||
        user: User = self._context["request"].user
 | 
					        request: HttpRequest = self.context.get("request")
 | 
				
			||||||
 | 
					        if not request and SERIALIZER_CONTEXT_BLUEPRINT in self.context:
 | 
				
			||||||
 | 
					            return app
 | 
				
			||||||
 | 
					        user = request.user
 | 
				
			||||||
        if user.has_perm("view_application", app) or user.has_perm(
 | 
					        if user.has_perm("view_application", app) or user.has_perm(
 | 
				
			||||||
            "authentik_core.view_application"
 | 
					            "authentik_core.view_application"
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
 | 
				
			|||||||
@ -2,16 +2,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from typing import TypedDict
 | 
					from typing import TypedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from guardian.utils import get_anonymous_user
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.fields import SerializerMethodField
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet
 | 
					from rest_framework.viewsets import GenericViewSet
 | 
				
			||||||
from ua_parser import user_agent_parser
 | 
					from ua_parser import user_agent_parser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerSuperuserPermissions
 | 
					 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.core.models import AuthenticatedSession
 | 
					from authentik.core.models import AuthenticatedSession
 | 
				
			||||||
@ -110,11 +106,4 @@ class AuthenticatedSessionViewSet(
 | 
				
			|||||||
    search_fields = ["user__username", "last_ip", "last_user_agent"]
 | 
					    search_fields = ["user__username", "last_ip", "last_user_agent"]
 | 
				
			||||||
    filterset_fields = ["user__username", "last_ip", "last_user_agent"]
 | 
					    filterset_fields = ["user__username", "last_ip", "last_user_agent"]
 | 
				
			||||||
    ordering = ["user__username"]
 | 
					    ordering = ["user__username"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_queryset(self):
 | 
					 | 
				
			||||||
        user = self.request.user if self.request else get_anonymous_user()
 | 
					 | 
				
			||||||
        if user.is_superuser:
 | 
					 | 
				
			||||||
            return super().get_queryset()
 | 
					 | 
				
			||||||
        return super().get_queryset().filter(user=user.pk)
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -2,19 +2,16 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from collections.abc import Iterable
 | 
					from collections.abc import Iterable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
 | 
					from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.parsers import MultiPartParser
 | 
					from rest_framework.parsers import MultiPartParser
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet
 | 
					from rest_framework.viewsets import GenericViewSet
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
 | 
					 | 
				
			||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 | 
					from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 | 
				
			||||||
from authentik.core.api.object_types import TypesMixin
 | 
					from authentik.core.api.object_types import TypesMixin
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
@ -189,11 +186,10 @@ class UserSourceConnectionViewSet(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = UserSourceConnection.objects.all()
 | 
					    queryset = UserSourceConnection.objects.all()
 | 
				
			||||||
    serializer_class = UserSourceConnectionSerializer
 | 
					    serializer_class = UserSourceConnectionSerializer
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					 | 
				
			||||||
    filterset_fields = ["user", "source__slug"]
 | 
					    filterset_fields = ["user", "source__slug"]
 | 
				
			||||||
    search_fields = ["source__slug"]
 | 
					    search_fields = ["source__slug"]
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
    ordering = ["source__slug", "pk"]
 | 
					    ordering = ["source__slug", "pk"]
 | 
				
			||||||
 | 
					    owner_field = "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupSourceConnectionSerializer(SourceSerializer):
 | 
					class GroupSourceConnectionSerializer(SourceSerializer):
 | 
				
			||||||
@ -228,8 +224,7 @@ class GroupSourceConnectionViewSet(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = GroupSourceConnection.objects.all()
 | 
					    queryset = GroupSourceConnection.objects.all()
 | 
				
			||||||
    serializer_class = GroupSourceConnectionSerializer
 | 
					    serializer_class = GroupSourceConnectionSerializer
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					 | 
				
			||||||
    filterset_fields = ["group", "source__slug"]
 | 
					    filterset_fields = ["group", "source__slug"]
 | 
				
			||||||
    search_fields = ["source__slug"]
 | 
					    search_fields = ["source__slug"]
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
    ordering = ["source__slug", "pk"]
 | 
					    ordering = ["source__slug", "pk"]
 | 
				
			||||||
 | 
					    owner_field = "user"
 | 
				
			||||||
 | 
				
			|||||||
@ -3,18 +3,15 @@
 | 
				
			|||||||
from typing import Any
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
 | 
				
			||||||
from guardian.shortcuts import assign_perm, get_anonymous_user
 | 
					from guardian.shortcuts import assign_perm, get_anonymous_user
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.exceptions import ValidationError
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
from rest_framework.fields import CharField
 | 
					from rest_framework.fields import CharField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerSuperuserPermissions
 | 
					 | 
				
			||||||
from authentik.blueprints.api import ManagedSerializer
 | 
					from authentik.blueprints.api import ManagedSerializer
 | 
				
			||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 | 
					from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
@ -138,8 +135,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        "managed",
 | 
					        "managed",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    ordering = ["identifier", "expires"]
 | 
					    ordering = ["identifier", "expires"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					    rbac_allow_create_without_perm = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        user = self.request.user if self.request else get_anonymous_user()
 | 
					        user = self.request.user if self.request else get_anonymous_user()
 | 
				
			||||||
 | 
				
			|||||||
@ -585,7 +585,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        """Set password for user"""
 | 
					        """Set password for user"""
 | 
				
			||||||
        user: User = self.get_object()
 | 
					        user: User = self.get_object()
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            user.set_password(request.data.get("password"))
 | 
					            user.set_password(request.data.get("password"), request=request)
 | 
				
			||||||
            user.save()
 | 
					            user.save()
 | 
				
			||||||
        except (ValidationError, IntegrityError) as exc:
 | 
					        except (ValidationError, IntegrityError) as exc:
 | 
				
			||||||
            LOGGER.debug("Failed to set password", exc=exc)
 | 
					            LOGGER.debug("Failed to set password", exc=exc)
 | 
				
			||||||
 | 
				
			|||||||
@ -44,13 +44,12 @@ class TokenBackend(InbuiltBackend):
 | 
				
			|||||||
        self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
 | 
					        self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
 | 
				
			||||||
    ) -> User | None:
 | 
					    ) -> User | None:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
 | 
					 | 
				
			||||||
            user = User._default_manager.get_by_natural_key(username)
 | 
					            user = User._default_manager.get_by_natural_key(username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except User.DoesNotExist:
 | 
					        except User.DoesNotExist:
 | 
				
			||||||
            # Run the default password hasher once to reduce the timing
 | 
					            # Run the default password hasher once to reduce the timing
 | 
				
			||||||
            # difference between an existing and a nonexistent user (#20760).
 | 
					            # difference between an existing and a nonexistent user (#20760).
 | 
				
			||||||
            User().set_password(password)
 | 
					            User().set_password(password, request=request)
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tokens = Token.filter_not_expired(
 | 
					        tokens = Token.filter_not_expired(
 | 
				
			||||||
 | 
				
			|||||||
@ -58,6 +58,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
 | 
				
			|||||||
            self._context["user"] = user
 | 
					            self._context["user"] = user
 | 
				
			||||||
        if request:
 | 
					        if request:
 | 
				
			||||||
            req.http_request = request
 | 
					            req.http_request = request
 | 
				
			||||||
 | 
					            self._context["http_request"] = request
 | 
				
			||||||
        req.context.update(**kwargs)
 | 
					        req.context.update(**kwargs)
 | 
				
			||||||
        self._context["request"] = req
 | 
					        self._context["request"] = req
 | 
				
			||||||
        self._context.update(**kwargs)
 | 
					        self._context.update(**kwargs)
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,9 @@ from authentik.events.middleware import should_log_model
 | 
				
			|||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.events.utils import model_to_dict
 | 
					from authentik.events.utils import model_to_dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BANNER_TEXT = f"""### authentik shell ({get_full_version()})
 | 
					
 | 
				
			||||||
 | 
					def get_banner_text(shell_type="shell") -> str:
 | 
				
			||||||
 | 
					    return f"""### authentik {shell_type} ({get_full_version()})
 | 
				
			||||||
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
 | 
					### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -114,4 +116,4 @@ class Command(BaseCommand):
 | 
				
			|||||||
            readline.parse_and_bind("tab: complete")
 | 
					            readline.parse_and_bind("tab: complete")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Run interactive shell
 | 
					        # Run interactive shell
 | 
				
			||||||
        code.interact(banner=BANNER_TEXT, local=namespace)
 | 
					        code.interact(banner=get_banner_text(), local=namespace)
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0041_applicationentitlement"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authenticatedsession",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_c_expires_08251d_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authenticatedsession",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_c_expirin_9cd839_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authenticatedsession",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_c_expirin_195a84_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authenticatedsession",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["session_key"], name="authentik_c_session_d0f005_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_c_expires_a62b4b_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_c_expirin_a1b838_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_c_expirin_ba04d9_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -356,13 +356,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
 | 
				
			|||||||
        """superuser == staff user"""
 | 
					        """superuser == staff user"""
 | 
				
			||||||
        return self.is_superuser  # type: ignore
 | 
					        return self.is_superuser  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_password(self, raw_password, signal=True, sender=None):
 | 
					    def set_password(self, raw_password, signal=True, sender=None, request=None):
 | 
				
			||||||
        if self.pk and signal:
 | 
					        if self.pk and signal:
 | 
				
			||||||
            from authentik.core.signals import password_changed
 | 
					            from authentik.core.signals import password_changed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if not sender:
 | 
					            if not sender:
 | 
				
			||||||
                sender = self
 | 
					                sender = self
 | 
				
			||||||
            password_changed.send(sender=sender, user=self, password=raw_password)
 | 
					            password_changed.send(sender=sender, user=self, password=raw_password, request=request)
 | 
				
			||||||
        self.password_change_date = now()
 | 
					        self.password_change_date = now()
 | 
				
			||||||
        return super().set_password(raw_password)
 | 
					        return super().set_password(raw_password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -846,6 +846,11 @@ class ExpiringModel(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        abstract = True
 | 
					        abstract = True
 | 
				
			||||||
 | 
					        indexes = [
 | 
				
			||||||
 | 
					            models.Index(fields=["expires"]),
 | 
				
			||||||
 | 
					            models.Index(fields=["expiring"]),
 | 
				
			||||||
 | 
					            models.Index(fields=["expiring", "expires"]),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def expire_action(self, *args, **kwargs):
 | 
					    def expire_action(self, *args, **kwargs):
 | 
				
			||||||
        """Handler which is called when this object is expired. By
 | 
					        """Handler which is called when this object is expired. By
 | 
				
			||||||
@ -901,7 +906,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Token")
 | 
					        verbose_name = _("Token")
 | 
				
			||||||
        verbose_name_plural = _("Tokens")
 | 
					        verbose_name_plural = _("Tokens")
 | 
				
			||||||
        indexes = [
 | 
					        indexes = ExpiringModel.Meta.indexes + [
 | 
				
			||||||
            models.Index(fields=["identifier"]),
 | 
					            models.Index(fields=["identifier"]),
 | 
				
			||||||
            models.Index(fields=["key"]),
 | 
					            models.Index(fields=["key"]),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
@ -1001,6 +1006,9 @@ class AuthenticatedSession(ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Authenticated Session")
 | 
					        verbose_name = _("Authenticated Session")
 | 
				
			||||||
        verbose_name_plural = _("Authenticated Sessions")
 | 
					        verbose_name_plural = _("Authenticated Sessions")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes + [
 | 
				
			||||||
 | 
					            models.Index(fields=["session_key"]),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
        return f"Authenticated Session {self.session_key[:10]}"
 | 
					        return f"Authenticated Session {self.session_key[:10]}"
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,6 @@ from rest_framework.validators import UniqueValidator
 | 
				
			|||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import SecretKeyFilter
 | 
					 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
 | 
					from authentik.core.api.utils import ModelSerializer, PassiveSerializer
 | 
				
			||||||
from authentik.crypto.apps import MANAGED_KEY
 | 
					from authentik.crypto.apps import MANAGED_KEY
 | 
				
			||||||
@ -36,7 +35,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
 | 
				
			|||||||
from authentik.crypto.models import CertificateKeyPair
 | 
					from authentik.crypto.models import CertificateKeyPair
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.rbac.decorators import permission_required
 | 
					from authentik.rbac.decorators import permission_required
 | 
				
			||||||
from authentik.rbac.filters import ObjectFilter
 | 
					from authentik.rbac.filters import ObjectFilter, SecretKeyFilter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_enterprise", "0003_remove_licenseusage_within_limits_and_more"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="licenseusage",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_e_expires_3f2956_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="licenseusage",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_e_expirin_11d3d7_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="licenseusage",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_e_expirin_4d558f_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -93,3 +93,4 @@ class LicenseUsage(ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("License Usage")
 | 
					        verbose_name = _("License Usage")
 | 
				
			||||||
        verbose_name_plural = _("License Usage Records")
 | 
					        verbose_name_plural = _("License Usage Records")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,8 @@
 | 
				
			|||||||
"""RAC Provider API Views"""
 | 
					"""RAC Provider API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet
 | 
					from rest_framework.viewsets import GenericViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
 | 
					 | 
				
			||||||
from authentik.core.api.groups import GroupMemberSerializer
 | 
					from authentik.core.api.groups import GroupMemberSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
@ -34,12 +31,6 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConnectionTokenOwnerFilter(OwnerFilter):
 | 
					 | 
				
			||||||
    """Owner filter for connection tokens (checks session's user)"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    owner_key = "session__user"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ConnectionTokenViewSet(
 | 
					class ConnectionTokenViewSet(
 | 
				
			||||||
    mixins.RetrieveModelMixin,
 | 
					    mixins.RetrieveModelMixin,
 | 
				
			||||||
    mixins.UpdateModelMixin,
 | 
					    mixins.UpdateModelMixin,
 | 
				
			||||||
@ -55,10 +46,4 @@ class ConnectionTokenViewSet(
 | 
				
			|||||||
    filterset_fields = ["endpoint", "session__user", "provider"]
 | 
					    filterset_fields = ["endpoint", "session__user", "provider"]
 | 
				
			||||||
    search_fields = ["endpoint__name", "provider__name"]
 | 
					    search_fields = ["endpoint__name", "provider__name"]
 | 
				
			||||||
    ordering = ["endpoint__name", "provider__name"]
 | 
					    ordering = ["endpoint__name", "provider__name"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					    owner_field = "session__user"
 | 
				
			||||||
    filter_backends = [
 | 
					 | 
				
			||||||
        ConnectionTokenOwnerFilter,
 | 
					 | 
				
			||||||
        DjangoFilterBackend,
 | 
					 | 
				
			||||||
        OrderingFilter,
 | 
					 | 
				
			||||||
        SearchFilter,
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
 | 
				
			||||||
 | 
					        ("authentik_providers_rac", "0005_alter_racpropertymapping_options"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="connectiontoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_p_expires_91f148_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="connectiontoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_p_expirin_59a5a7_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="connectiontoken",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_p_expirin_aed3ca_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -159,9 +159,9 @@ class ConnectionToken(ExpiringModel):
 | 
				
			|||||||
            default_settings["port"] = str(port)
 | 
					            default_settings["port"] = str(port)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            default_settings["hostname"] = self.endpoint.host
 | 
					            default_settings["hostname"] = self.endpoint.host
 | 
				
			||||||
        default_settings["client-name"] = "authentik"
 | 
					        if self.endpoint.protocol == Protocols.RDP:
 | 
				
			||||||
        # default_settings["enable-drive"] = "true"
 | 
					            default_settings["resize-method"] = "display-update"
 | 
				
			||||||
        # default_settings["drive-name"] = "authentik"
 | 
					        default_settings["client-name"] = f"authentik - {self.session.user}"
 | 
				
			||||||
        settings = {}
 | 
					        settings = {}
 | 
				
			||||||
        always_merger.merge(settings, default_settings)
 | 
					        always_merger.merge(settings, default_settings)
 | 
				
			||||||
        always_merger.merge(settings, self.endpoint.provider.settings)
 | 
					        always_merger.merge(settings, self.endpoint.provider.settings)
 | 
				
			||||||
@ -211,3 +211,4 @@ class ConnectionToken(ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("RAC Connection token")
 | 
					        verbose_name = _("RAC Connection token")
 | 
				
			||||||
        verbose_name_plural = _("RAC Connection tokens")
 | 
					        verbose_name_plural = _("RAC Connection tokens")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
				
			|||||||
@ -50,9 +50,10 @@ class TestModels(TransactionTestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "hostname": self.endpoint.host.split(":")[0],
 | 
					                "hostname": self.endpoint.host.split(":")[0],
 | 
				
			||||||
                "port": "1324",
 | 
					                "port": "1324",
 | 
				
			||||||
                "client-name": "authentik",
 | 
					                "client-name": f"authentik - {self.user}",
 | 
				
			||||||
                "drive-path": path,
 | 
					                "drive-path": path,
 | 
				
			||||||
                "create-drive-path": "true",
 | 
					                "create-drive-path": "true",
 | 
				
			||||||
 | 
					                "resize-method": "display-update",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Set settings in provider
 | 
					        # Set settings in provider
 | 
				
			||||||
@ -63,10 +64,11 @@ class TestModels(TransactionTestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "hostname": self.endpoint.host.split(":")[0],
 | 
					                "hostname": self.endpoint.host.split(":")[0],
 | 
				
			||||||
                "port": "1324",
 | 
					                "port": "1324",
 | 
				
			||||||
                "client-name": "authentik",
 | 
					                "client-name": f"authentik - {self.user}",
 | 
				
			||||||
                "drive-path": path,
 | 
					                "drive-path": path,
 | 
				
			||||||
                "create-drive-path": "true",
 | 
					                "create-drive-path": "true",
 | 
				
			||||||
                "level": "provider",
 | 
					                "level": "provider",
 | 
				
			||||||
 | 
					                "resize-method": "display-update",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Set settings in endpoint
 | 
					        # Set settings in endpoint
 | 
				
			||||||
@ -79,10 +81,11 @@ class TestModels(TransactionTestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "hostname": self.endpoint.host.split(":")[0],
 | 
					                "hostname": self.endpoint.host.split(":")[0],
 | 
				
			||||||
                "port": "1324",
 | 
					                "port": "1324",
 | 
				
			||||||
                "client-name": "authentik",
 | 
					                "client-name": f"authentik - {self.user}",
 | 
				
			||||||
                "drive-path": path,
 | 
					                "drive-path": path,
 | 
				
			||||||
                "create-drive-path": "true",
 | 
					                "create-drive-path": "true",
 | 
				
			||||||
                "level": "endpoint",
 | 
					                "level": "endpoint",
 | 
				
			||||||
 | 
					                "resize-method": "display-update",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Set settings in token
 | 
					        # Set settings in token
 | 
				
			||||||
@ -95,10 +98,11 @@ class TestModels(TransactionTestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "hostname": self.endpoint.host.split(":")[0],
 | 
					                "hostname": self.endpoint.host.split(":")[0],
 | 
				
			||||||
                "port": "1324",
 | 
					                "port": "1324",
 | 
				
			||||||
                "client-name": "authentik",
 | 
					                "client-name": f"authentik - {self.user}",
 | 
				
			||||||
                "drive-path": path,
 | 
					                "drive-path": path,
 | 
				
			||||||
                "create-drive-path": "true",
 | 
					                "create-drive-path": "true",
 | 
				
			||||||
                "level": "token",
 | 
					                "level": "token",
 | 
				
			||||||
 | 
					                "resize-method": "display-update",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Set settings in property mapping (provider)
 | 
					        # Set settings in property mapping (provider)
 | 
				
			||||||
@ -114,10 +118,11 @@ class TestModels(TransactionTestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "hostname": self.endpoint.host.split(":")[0],
 | 
					                "hostname": self.endpoint.host.split(":")[0],
 | 
				
			||||||
                "port": "1324",
 | 
					                "port": "1324",
 | 
				
			||||||
                "client-name": "authentik",
 | 
					                "client-name": f"authentik - {self.user}",
 | 
				
			||||||
                "drive-path": path,
 | 
					                "drive-path": path,
 | 
				
			||||||
                "create-drive-path": "true",
 | 
					                "create-drive-path": "true",
 | 
				
			||||||
                "level": "property_mapping_provider",
 | 
					                "level": "property_mapping_provider",
 | 
				
			||||||
 | 
					                "resize-method": "display-update",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Set settings in property mapping (endpoint)
 | 
					        # Set settings in property mapping (endpoint)
 | 
				
			||||||
@ -135,11 +140,12 @@ class TestModels(TransactionTestCase):
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                "hostname": self.endpoint.host.split(":")[0],
 | 
					                "hostname": self.endpoint.host.split(":")[0],
 | 
				
			||||||
                "port": "1324",
 | 
					                "port": "1324",
 | 
				
			||||||
                "client-name": "authentik",
 | 
					                "client-name": f"authentik - {self.user}",
 | 
				
			||||||
                "drive-path": path,
 | 
					                "drive-path": path,
 | 
				
			||||||
                "create-drive-path": "true",
 | 
					                "create-drive-path": "true",
 | 
				
			||||||
                "level": "property_mapping_endpoint",
 | 
					                "level": "property_mapping_endpoint",
 | 
				
			||||||
                "foo": "true",
 | 
					                "foo": "true",
 | 
				
			||||||
                "bar": "6",
 | 
					                "bar": "6",
 | 
				
			||||||
 | 
					                "resize-method": "display-update",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,11 @@
 | 
				
			|||||||
"""AuthenticatorEndpointGDTCStage API Views"""
 | 
					"""AuthenticatorEndpointGDTCStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.permissions import IsAdminUser
 | 
					from rest_framework.permissions import IsAdminUser
 | 
				
			||||||
from rest_framework.serializers import ModelSerializer
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.enterprise.api import EnterpriseRequiredMixin
 | 
					from authentik.enterprise.api import EnterpriseRequiredMixin
 | 
				
			||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
 | 
					from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
 | 
				
			||||||
@ -67,8 +64,7 @@ class EndpointDeviceViewSet(
 | 
				
			|||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
    filterset_fields = ["name"]
 | 
					    filterset_fields = ["name"]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EndpointAdminDeviceViewSet(ModelViewSet):
 | 
					class EndpointAdminDeviceViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,15 @@
 | 
				
			|||||||
"""Notification API Views"""
 | 
					"""Notification API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.fields import ReadOnlyField
 | 
					from rest_framework.fields import ReadOnlyField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet
 | 
					from rest_framework.viewsets import GenericViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.events.api.events import EventSerializer
 | 
					from authentik.events.api.events import EventSerializer
 | 
				
			||||||
@ -57,8 +55,7 @@ class NotificationViewSet(
 | 
				
			|||||||
        "seen",
 | 
					        "seen",
 | 
				
			||||||
        "user",
 | 
					        "user",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema(
 | 
					    @extend_schema(
 | 
				
			||||||
        request=OpenApiTypes.NONE,
 | 
					        request=OpenApiTypes.NONE,
 | 
				
			||||||
@ -66,7 +63,7 @@ class NotificationViewSet(
 | 
				
			|||||||
            204: OpenApiResponse(description="Marked tasks as read successfully."),
 | 
					            204: OpenApiResponse(description="Marked tasks as read successfully."),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    @action(detail=False, methods=["post"])
 | 
					    @action(detail=False, methods=["post"], permission_classes=[IsAuthenticated])
 | 
				
			||||||
    def mark_all_seen(self, request: Request) -> Response:
 | 
					    def mark_all_seen(self, request: Request) -> Response:
 | 
				
			||||||
        """Mark all the user's notifications as seen"""
 | 
					        """Mark all the user's notifications as seen"""
 | 
				
			||||||
        Notification.objects.filter(user=request.user, seen=False).update(seen=True)
 | 
					        Notification.objects.filter(user=request.user, seen=False).update(seen=True)
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_e_expires_8c73a8_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_e_expirin_b5cb5e_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_e_expirin_e37180_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="systemtask",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_e_expires_4d3985_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="systemtask",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_e_expirin_81d649_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="systemtask",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_e_expirin_eb3598_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -306,7 +306,7 @@ class Event(SerializerModel, ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Event")
 | 
					        verbose_name = _("Event")
 | 
				
			||||||
        verbose_name_plural = _("Events")
 | 
					        verbose_name_plural = _("Events")
 | 
				
			||||||
        indexes = [
 | 
					        indexes = ExpiringModel.Meta.indexes + [
 | 
				
			||||||
            models.Index(fields=["action"]),
 | 
					            models.Index(fields=["action"]),
 | 
				
			||||||
            models.Index(fields=["user"]),
 | 
					            models.Index(fields=["user"]),
 | 
				
			||||||
            models.Index(fields=["app"]),
 | 
					            models.Index(fields=["app"]),
 | 
				
			||||||
@ -694,3 +694,4 @@ class SystemTask(SerializerModel, ExpiringModel):
 | 
				
			|||||||
        permissions = [("run_task", _("Run task"))]
 | 
					        permissions = [("run_task", _("Run task"))]
 | 
				
			||||||
        verbose_name = _("System Task")
 | 
					        verbose_name = _("System Task")
 | 
				
			||||||
        verbose_name_plural = _("System Tasks")
 | 
					        verbose_name_plural = _("System Tasks")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
				
			|||||||
@ -106,9 +106,9 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(password_changed)
 | 
					@receiver(password_changed)
 | 
				
			||||||
def on_password_changed(sender, user: User, password: str, **_):
 | 
					def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_):
 | 
				
			||||||
    """Log password change"""
 | 
					    """Log password change"""
 | 
				
			||||||
    Event.new(EventAction.PASSWORD_SET).from_http(None, user=user)
 | 
					    Event.new(EventAction.PASSWORD_SET).from_http(request, user=user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save, sender=Event)
 | 
					@receiver(post_save, sender=Event)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
"""Flow Stage API Views"""
 | 
					"""Flow Stage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.urls.base import reverse
 | 
					from django.urls.base import reverse
 | 
				
			||||||
from drf_spectacular.utils import extend_schema
 | 
					from drf_spectacular.utils import extend_schema
 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
@ -27,6 +29,11 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
 | 
				
			|||||||
    component = SerializerMethodField()
 | 
					    component = SerializerMethodField()
 | 
				
			||||||
    flow_set = FlowSetSerializer(many=True, required=False)
 | 
					    flow_set = FlowSetSerializer(many=True, required=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_representation(self, instance: Stage):
 | 
				
			||||||
 | 
					        if isinstance(instance, Stage) and instance.is_in_memory:
 | 
				
			||||||
 | 
					            instance.stage_uuid = uuid4()
 | 
				
			||||||
 | 
					        return super().to_representation(instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_component(self, obj: Stage) -> str:
 | 
					    def get_component(self, obj: Stage) -> str:
 | 
				
			||||||
        """Get object type so that we know how to edit the object"""
 | 
					        """Get object type so that we know how to edit the object"""
 | 
				
			||||||
        if obj.__class__ == Stage:
 | 
					        if obj.__class__ == Stage:
 | 
				
			||||||
 | 
				
			|||||||
@ -88,7 +88,8 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            model_name="flowstagebinding",
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
            name="re_evaluate_policies",
 | 
					            name="re_evaluate_policies",
 | 
				
			||||||
            field=models.BooleanField(
 | 
					            field=models.BooleanField(
 | 
				
			||||||
                default=False, help_text="Evaluate policies when the Stage is present to the user."
 | 
					                default=False,
 | 
				
			||||||
 | 
					                help_text="Evaluate policies when the Stage is presented to the user.",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            model_name="flowstagebinding",
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
            name="re_evaluate_policies",
 | 
					            name="re_evaluate_policies",
 | 
				
			||||||
            field=models.BooleanField(
 | 
					            field=models.BooleanField(
 | 
				
			||||||
                default=True, help_text="Evaluate policies when the Stage is present to the user."
 | 
					                default=True, help_text="Evaluate policies when the Stage is presented to the user."
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -102,8 +102,12 @@ class Stage(SerializerModel):
 | 
				
			|||||||
        user settings are available, or a challenge."""
 | 
					        user settings are available, or a challenge."""
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_in_memory(self):
 | 
				
			||||||
 | 
					        return hasattr(self, "__in_memory_type")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        if hasattr(self, "__in_memory_type"):
 | 
					        if self.is_in_memory:
 | 
				
			||||||
            return f"In-memory Stage {getattr(self, '__in_memory_type')}"
 | 
					            return f"In-memory Stage {getattr(self, '__in_memory_type')}"
 | 
				
			||||||
        return f"Stage {self.name}"
 | 
					        return f"Stage {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -227,7 +231,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    re_evaluate_policies = models.BooleanField(
 | 
					    re_evaluate_policies = models.BooleanField(
 | 
				
			||||||
        default=True,
 | 
					        default=True,
 | 
				
			||||||
        help_text=_("Evaluate policies when the Stage is present to the user."),
 | 
					        help_text=_("Evaluate policies when the Stage is presented to the user."),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invalid_response_action = models.TextField(
 | 
					    invalid_response_action = models.TextField(
 | 
				
			||||||
 | 
				
			|||||||
@ -159,9 +159,17 @@ class FlowPlan:
 | 
				
			|||||||
            stage = final_stage(request=request, executor=temp_exec)
 | 
					            stage = final_stage(request=request, executor=temp_exec)
 | 
				
			||||||
            return stage.dispatch(request)
 | 
					            return stage.dispatch(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        get_qs = request.GET.copy()
 | 
				
			||||||
 | 
					        if request.user.is_authenticated and (
 | 
				
			||||||
 | 
					            # Object-scoped permission or global permission
 | 
				
			||||||
 | 
					            request.user.has_perm("authentik_flows.inspect_flow", flow)
 | 
				
			||||||
 | 
					            or request.user.has_perm("authentik_flows.inspect_flow")
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            get_qs["inspector"] = "available"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect_with_qs(
 | 
					        return redirect_with_qs(
 | 
				
			||||||
            "authentik_core:if-flow",
 | 
					            "authentik_core:if-flow",
 | 
				
			||||||
            request.GET,
 | 
					            get_qs,
 | 
				
			||||||
            flow_slug=flow.slug,
 | 
					            flow_slug=flow.slug,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,8 +7,8 @@ from django.http import HttpRequest, HttpResponse
 | 
				
			|||||||
from django.test.client import RequestFactory
 | 
					from django.test.client import RequestFactory
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import Group, User
 | 
				
			||||||
from authentik.core.tests.utils import create_test_flow
 | 
					from authentik.core.tests.utils import create_test_flow, create_test_user
 | 
				
			||||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
 | 
					from authentik.flows.markers import ReevaluateMarker, StageMarker
 | 
				
			||||||
from authentik.flows.models import (
 | 
					from authentik.flows.models import (
 | 
				
			||||||
    FlowDeniedAction,
 | 
					    FlowDeniedAction,
 | 
				
			||||||
@ -255,7 +255,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding = FlowStageBinding.objects.create(
 | 
					        binding = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding2 = FlowStageBinding.objects.create(
 | 
					        binding2 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
@ -278,8 +282,8 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[0], binding)
 | 
					            self.assertEqual(plan.bindings[0], binding)
 | 
				
			||||||
            self.assertEqual(plan.bindings[1], binding2)
 | 
					            self.assertEqual(plan.bindings[1], binding2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, StageMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
					            self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Second request, this passes the first dummy stage
 | 
					            # Second request, this passes the first dummy stage
 | 
				
			||||||
            response = self.client.post(exec_url)
 | 
					            response = self.client.post(exec_url)
 | 
				
			||||||
@ -301,7 +305,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding = FlowStageBinding.objects.create(
 | 
					        binding = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding2 = FlowStageBinding.objects.create(
 | 
					        binding2 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
@ -310,7 +318,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            re_evaluate_policies=True,
 | 
					            re_evaluate_policies=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding3 = FlowStageBinding.objects.create(
 | 
					        binding3 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=2,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
 | 
					        PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
 | 
				
			||||||
@ -328,9 +340,9 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[1], binding2)
 | 
					            self.assertEqual(plan.bindings[1], binding2)
 | 
				
			||||||
            self.assertEqual(plan.bindings[2], binding3)
 | 
					            self.assertEqual(plan.bindings[2], binding3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, StageMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
					            self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[2], StageMarker)
 | 
					            self.assertEqual(plan.markers[2].__class__, StageMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Second request, this passes the first dummy stage
 | 
					            # Second request, this passes the first dummy stage
 | 
				
			||||||
            response = self.client.post(exec_url)
 | 
					            response = self.client.post(exec_url)
 | 
				
			||||||
@ -341,8 +353,8 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[0], binding2)
 | 
					            self.assertEqual(plan.bindings[0], binding2)
 | 
				
			||||||
            self.assertEqual(plan.bindings[1], binding3)
 | 
					            self.assertEqual(plan.bindings[1], binding3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], StageMarker)
 | 
					            self.assertEqual(plan.markers[1].__class__, StageMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # third request, this should trigger the re-evaluate
 | 
					        # third request, this should trigger the re-evaluate
 | 
				
			||||||
        # We do this request without the patch, so the policy results in false
 | 
					        # We do this request without the patch, so the policy results in false
 | 
				
			||||||
@ -360,7 +372,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding = FlowStageBinding.objects.create(
 | 
					        binding = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding2 = FlowStageBinding.objects.create(
 | 
					        binding2 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
@ -369,7 +385,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            re_evaluate_policies=True,
 | 
					            re_evaluate_policies=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding3 = FlowStageBinding.objects.create(
 | 
					        binding3 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=2,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
 | 
					        PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
 | 
				
			||||||
@ -387,9 +407,9 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[1], binding2)
 | 
					            self.assertEqual(plan.bindings[1], binding2)
 | 
				
			||||||
            self.assertEqual(plan.bindings[2], binding3)
 | 
					            self.assertEqual(plan.bindings[2], binding3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, StageMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
					            self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[2], StageMarker)
 | 
					            self.assertEqual(plan.markers[2].__class__, StageMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Second request, this passes the first dummy stage
 | 
					            # Second request, this passes the first dummy stage
 | 
				
			||||||
            response = self.client.post(exec_url)
 | 
					            response = self.client.post(exec_url)
 | 
				
			||||||
@ -400,8 +420,8 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[0], binding2)
 | 
					            self.assertEqual(plan.bindings[0], binding2)
 | 
				
			||||||
            self.assertEqual(plan.bindings[1], binding3)
 | 
					            self.assertEqual(plan.bindings[1], binding3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], StageMarker)
 | 
					            self.assertEqual(plan.markers[1].__class__, StageMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Third request, this passes the first dummy stage
 | 
					            # Third request, this passes the first dummy stage
 | 
				
			||||||
            response = self.client.post(exec_url)
 | 
					            response = self.client.post(exec_url)
 | 
				
			||||||
@ -411,7 +431,7 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            self.assertEqual(plan.bindings[0], binding3)
 | 
					            self.assertEqual(plan.bindings[0], binding3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, StageMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # third request, this should trigger the re-evaluate
 | 
					        # third request, this should trigger the re-evaluate
 | 
				
			||||||
        # We do this request without the patch, so the policy results in false
 | 
					        # We do this request without the patch, so the policy results in false
 | 
				
			||||||
@ -429,7 +449,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding = FlowStageBinding.objects.create(
 | 
					        binding = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding2 = FlowStageBinding.objects.create(
 | 
					        binding2 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
@ -444,7 +468,11 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            re_evaluate_policies=True,
 | 
					            re_evaluate_policies=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding4 = FlowStageBinding.objects.create(
 | 
					        binding4 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=2,
 | 
				
			||||||
 | 
					            evaluate_on_plan=True,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
 | 
					        PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
 | 
				
			||||||
@ -465,10 +493,10 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[2], binding3)
 | 
					            self.assertEqual(plan.bindings[2], binding3)
 | 
				
			||||||
            self.assertEqual(plan.bindings[3], binding4)
 | 
					            self.assertEqual(plan.bindings[3], binding4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertEqual(plan.markers[0].__class__, StageMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
					            self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[2], ReevaluateMarker)
 | 
					            self.assertEqual(plan.markers[2].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[3], StageMarker)
 | 
					            self.assertEqual(plan.markers[3].__class__, StageMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Second request, this passes the first dummy stage
 | 
					        # Second request, this passes the first dummy stage
 | 
				
			||||||
        response = self.client.post(exec_url)
 | 
					        response = self.client.post(exec_url)
 | 
				
			||||||
@ -519,9 +547,9 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        # Stage 0 is a deny stage that is added dynamically
 | 
					        # Stage 0 is a deny stage that is added dynamically
 | 
				
			||||||
        # when the reputation policy says so
 | 
					        # when the reputation policy says so
 | 
				
			||||||
        deny_stage = DenyStage.objects.create(name="deny")
 | 
					        deny_stage = DenyStage.objects.create(name=generate_id())
 | 
				
			||||||
        reputation_policy = ReputationPolicy.objects.create(
 | 
					        reputation_policy = ReputationPolicy.objects.create(
 | 
				
			||||||
            name="reputation", threshold=-1, check_ip=False
 | 
					            name=generate_id(), threshold=-1, check_ip=False
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        deny_binding = FlowStageBinding.objects.create(
 | 
					        deny_binding = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
@ -534,7 +562,7 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Stage 1 is an identification stage
 | 
					        # Stage 1 is an identification stage
 | 
				
			||||||
        ident_stage = IdentificationStage.objects.create(
 | 
					        ident_stage = IdentificationStage.objects.create(
 | 
				
			||||||
            name="ident",
 | 
					            name=generate_id(),
 | 
				
			||||||
            user_fields=[UserFields.E_MAIL],
 | 
					            user_fields=[UserFields.E_MAIL],
 | 
				
			||||||
            pretend_user_exists=False,
 | 
					            pretend_user_exists=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -559,3 +587,64 @@ class TestFlowExecutor(FlowTestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True)
 | 
					        response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True)
 | 
				
			||||||
        self.assertStageResponse(response, flow, component="ak-stage-access-denied")
 | 
					        self.assertStageResponse(response, flow, component="ak-stage-access-denied")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_re_evaluate_group_binding(self):
 | 
				
			||||||
 | 
					        """Test re-evaluate stage binding that has a policy binding to a group"""
 | 
				
			||||||
 | 
					        flow = create_test_flow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user_group_membership = create_test_user()
 | 
				
			||||||
 | 
					        user_direct_binding = create_test_user()
 | 
				
			||||||
 | 
					        user_other = create_test_user()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        group_a = Group.objects.create(name=generate_id())
 | 
				
			||||||
 | 
					        user_group_membership.ak_groups.add(group_a)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Stage 0 is an identification stage
 | 
				
			||||||
 | 
					        ident_stage = IdentificationStage.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            user_fields=[UserFields.USERNAME],
 | 
				
			||||||
 | 
					            pretend_user_exists=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=ident_stage,
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Stage 1 is a dummy stage that is only shown for users in group_a
 | 
				
			||||||
 | 
					        dummy_stage = DummyStage.objects.create(name=generate_id())
 | 
				
			||||||
 | 
					        dummy_binding = FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1)
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(group=group_a, target=dummy_binding, order=0)
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(user=user_direct_binding, target=dummy_binding, order=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Stage 2 is a deny stage that (in this case) only user_b will see
 | 
				
			||||||
 | 
					        deny_stage = DenyStage.objects.create(name=generate_id())
 | 
				
			||||||
 | 
					        FlowStageBinding.objects.create(target=flow, stage=deny_stage, order=2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        exec_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with self.subTest(f"Test user access through group: {user_group_membership}"):
 | 
				
			||||||
 | 
					            self.client.logout()
 | 
				
			||||||
 | 
					            # First request, run the planner
 | 
				
			||||||
 | 
					            response = self.client.get(exec_url)
 | 
				
			||||||
 | 
					            self.assertStageResponse(response, flow, component="ak-stage-identification")
 | 
				
			||||||
 | 
					            response = self.client.post(
 | 
				
			||||||
 | 
					                exec_url, {"uid_field": user_group_membership.username}, follow=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            self.assertStageResponse(response, flow, component="ak-stage-dummy")
 | 
				
			||||||
 | 
					        with self.subTest(f"Test user access through user: {user_direct_binding}"):
 | 
				
			||||||
 | 
					            self.client.logout()
 | 
				
			||||||
 | 
					            # First request, run the planner
 | 
				
			||||||
 | 
					            response = self.client.get(exec_url)
 | 
				
			||||||
 | 
					            self.assertStageResponse(response, flow, component="ak-stage-identification")
 | 
				
			||||||
 | 
					            response = self.client.post(
 | 
				
			||||||
 | 
					                exec_url, {"uid_field": user_direct_binding.username}, follow=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            self.assertStageResponse(response, flow, component="ak-stage-dummy")
 | 
				
			||||||
 | 
					        with self.subTest(f"Test user has no access: {user_other}"):
 | 
				
			||||||
 | 
					            self.client.logout()
 | 
				
			||||||
 | 
					            # First request, run the planner
 | 
				
			||||||
 | 
					            response = self.client.get(exec_url)
 | 
				
			||||||
 | 
					            self.assertStageResponse(response, flow, component="ak-stage-identification")
 | 
				
			||||||
 | 
					            response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True)
 | 
				
			||||||
 | 
					            self.assertStageResponse(response, flow, component="ak-stage-access-denied")
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ from rest_framework.test import APITestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 | 
				
			||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction
 | 
					from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction
 | 
				
			||||||
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
from authentik.stages.dummy.models import DummyStage
 | 
					from authentik.stages.dummy.models import DummyStage
 | 
				
			||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
 | 
					from authentik.stages.identification.models import IdentificationStage, UserFields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,7 +27,7 @@ class TestFlowInspector(APITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Stage 1 is an identification stage
 | 
					        # Stage 1 is an identification stage
 | 
				
			||||||
        ident_stage = IdentificationStage.objects.create(
 | 
					        ident_stage = IdentificationStage.objects.create(
 | 
				
			||||||
            name="ident",
 | 
					            name=generate_id(),
 | 
				
			||||||
            user_fields=[UserFields.USERNAME],
 | 
					            user_fields=[UserFields.USERNAME],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        FlowStageBinding.objects.create(
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
@ -35,9 +36,8 @@ class TestFlowInspector(APITestCase):
 | 
				
			|||||||
            order=1,
 | 
					            order=1,
 | 
				
			||||||
            invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
 | 
					            invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        FlowStageBinding.objects.create(
 | 
					        dummy_stage = DummyStage.objects.create(name=generate_id())
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
 | 
					        FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1)
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        res = self.client.get(
 | 
					        res = self.client.get(
 | 
				
			||||||
            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
@ -68,9 +68,11 @@ class TestFlowInspector(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        content = loads(ins.content)
 | 
					        content = loads(ins.content)
 | 
				
			||||||
        self.assertEqual(content["is_completed"], False)
 | 
					        self.assertEqual(content["is_completed"], False)
 | 
				
			||||||
        self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident")
 | 
					 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2"
 | 
					            content["current_plan"]["current_stage"]["stage_obj"]["name"], ident_stage.name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], dummy_stage.name
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.client.post(
 | 
					        self.client.post(
 | 
				
			||||||
@ -84,8 +86,12 @@ class TestFlowInspector(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        content = loads(ins.content)
 | 
					        content = loads(ins.content)
 | 
				
			||||||
        self.assertEqual(content["is_completed"], False)
 | 
					        self.assertEqual(content["is_completed"], False)
 | 
				
			||||||
        self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
 | 
					        self.assertEqual(
 | 
				
			||||||
        self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
 | 
					            content["plans"][0]["current_stage"]["stage_obj"]["name"], ident_stage.name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            content["current_plan"]["current_stage"]["stage_obj"]["name"], dummy_stage.name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username
 | 
					            content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,7 @@ from authentik.flows.planner import (
 | 
				
			|||||||
    cache_key,
 | 
					    cache_key,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.flows.stage import StageView
 | 
					from authentik.flows.stage import StageView
 | 
				
			||||||
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
from authentik.lib.tests.utils import dummy_get_response
 | 
					from authentik.lib.tests.utils import dummy_get_response
 | 
				
			||||||
from authentik.outposts.apps import MANAGED_OUTPOST
 | 
					from authentik.outposts.apps import MANAGED_OUTPOST
 | 
				
			||||||
from authentik.outposts.models import Outpost
 | 
					from authentik.outposts.models import Outpost
 | 
				
			||||||
@ -153,7 +154,7 @@ class TestFlowPlanner(TestCase):
 | 
				
			|||||||
        """Test planner cache"""
 | 
					        """Test planner cache"""
 | 
				
			||||||
        flow = create_test_flow(FlowDesignation.AUTHENTICATION)
 | 
					        flow = create_test_flow(FlowDesignation.AUTHENTICATION)
 | 
				
			||||||
        FlowStageBinding.objects.create(
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
 | 
					            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        request = self.request_factory.get(
 | 
					        request = self.request_factory.get(
 | 
				
			||||||
            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
@ -172,7 +173,7 @@ class TestFlowPlanner(TestCase):
 | 
				
			|||||||
        """Test planner with default_context"""
 | 
					        """Test planner with default_context"""
 | 
				
			||||||
        flow = create_test_flow()
 | 
					        flow = create_test_flow()
 | 
				
			||||||
        FlowStageBinding.objects.create(
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
 | 
					            target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user = User.objects.create(username="test-user")
 | 
					        user = User.objects.create(username="test-user")
 | 
				
			||||||
@ -191,7 +192,7 @@ class TestFlowPlanner(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        FlowStageBinding.objects.create(
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
            stage=DummyStage.objects.create(name="dummy1"),
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
            order=0,
 | 
					            order=0,
 | 
				
			||||||
            re_evaluate_policies=True,
 | 
					            re_evaluate_policies=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -204,7 +205,7 @@ class TestFlowPlanner(TestCase):
 | 
				
			|||||||
        planner = FlowPlanner(flow)
 | 
					        planner = FlowPlanner(flow)
 | 
				
			||||||
        plan = planner.plan(request)
 | 
					        plan = planner.plan(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIsInstance(plan.markers[0], ReevaluateMarker)
 | 
					        self.assertEqual(plan.markers[0].__class__, ReevaluateMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_planner_reevaluate_actual(self):
 | 
					    def test_planner_reevaluate_actual(self):
 | 
				
			||||||
        """Test planner with re-evaluate"""
 | 
					        """Test planner with re-evaluate"""
 | 
				
			||||||
@ -212,11 +213,14 @@ class TestFlowPlanner(TestCase):
 | 
				
			|||||||
        false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
 | 
					        false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding = FlowStageBinding.objects.create(
 | 
					        binding = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
 | 
					            order=0,
 | 
				
			||||||
 | 
					            re_evaluate_policies=False,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding2 = FlowStageBinding.objects.create(
 | 
					        binding2 = FlowStageBinding.objects.create(
 | 
				
			||||||
            target=flow,
 | 
					            target=flow,
 | 
				
			||||||
            stage=DummyStage.objects.create(name="dummy2"),
 | 
					            stage=DummyStage.objects.create(name=generate_id()),
 | 
				
			||||||
            order=1,
 | 
					            order=1,
 | 
				
			||||||
            re_evaluate_policies=True,
 | 
					            re_evaluate_policies=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -240,6 +244,8 @@ class TestFlowPlanner(TestCase):
 | 
				
			|||||||
            self.assertEqual(plan.bindings[0], binding)
 | 
					            self.assertEqual(plan.bindings[0], binding)
 | 
				
			||||||
            self.assertEqual(plan.bindings[1], binding2)
 | 
					            self.assertEqual(plan.bindings[1], binding2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.assertEqual(plan.markers[0].__class__, StageMarker)
 | 
				
			||||||
 | 
					            self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
					            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
				
			||||||
            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
					            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -78,7 +78,9 @@ class FlowInspectorView(APIView):
 | 
				
			|||||||
        self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
 | 
					        self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
 | 
				
			||||||
        if settings.DEBUG:
 | 
					        if settings.DEBUG:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        if request.user.has_perm("authentik_flow.inspect_flow", self.flow):
 | 
					        if request.user.has_perm(
 | 
				
			||||||
 | 
					            "authentik_flows.inspect_flow", self.flow
 | 
				
			||||||
 | 
					        ) or request.user.has_perm("authentik_flows.inspect_flow"):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        raise Http404
 | 
					        raise Http404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -94,6 +96,9 @@ class FlowInspectorView(APIView):
 | 
				
			|||||||
        """Get current flow state and record it"""
 | 
					        """Get current flow state and record it"""
 | 
				
			||||||
        plans = []
 | 
					        plans = []
 | 
				
			||||||
        for plan in request.session.get(SESSION_KEY_HISTORY, []):
 | 
					        for plan in request.session.get(SESSION_KEY_HISTORY, []):
 | 
				
			||||||
 | 
					            plan: FlowPlan
 | 
				
			||||||
 | 
					            if plan.flow_pk != self.flow.pk.hex:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
            plan_serializer = FlowInspectorPlanSerializer(
 | 
					            plan_serializer = FlowInspectorPlanSerializer(
 | 
				
			||||||
                instance=plan, context={"request": request}
 | 
					                instance=plan, context={"request": request}
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -9,20 +9,25 @@ from typing import Any
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from cachetools import TLRUCache, cached
 | 
					from cachetools import TLRUCache, cached
 | 
				
			||||||
from django.core.exceptions import FieldError
 | 
					from django.core.exceptions import FieldError
 | 
				
			||||||
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.utils.text import slugify
 | 
					from django.utils.text import slugify
 | 
				
			||||||
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from guardian.shortcuts import get_anonymous_user
 | 
					from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
from rest_framework.serializers import ValidationError
 | 
					from rest_framework.serializers import ValidationError
 | 
				
			||||||
from sentry_sdk import start_span
 | 
					from sentry_sdk import start_span
 | 
				
			||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import AuthenticatedSession, User
 | 
				
			||||||
from authentik.events.models import Event
 | 
					from authentik.events.models import Event
 | 
				
			||||||
from authentik.lib.expression.exceptions import ControlFlowException
 | 
					from authentik.lib.expression.exceptions import ControlFlowException
 | 
				
			||||||
from authentik.lib.utils.http import get_http_session
 | 
					from authentik.lib.utils.http import get_http_session
 | 
				
			||||||
 | 
					from authentik.lib.utils.time import timedelta_from_string
 | 
				
			||||||
from authentik.policies.models import Policy, PolicyBinding
 | 
					from authentik.policies.models import Policy, PolicyBinding
 | 
				
			||||||
from authentik.policies.process import PolicyProcess
 | 
					from authentik.policies.process import PolicyProcess
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.id_token import IDToken
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
 | 
				
			||||||
from authentik.stages.authenticator import devices_for_user
 | 
					from authentik.stages.authenticator import devices_for_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
@ -56,6 +61,7 @@ class BaseEvaluator:
 | 
				
			|||||||
            "ak_logger": get_logger(self._filename).bind(),
 | 
					            "ak_logger": get_logger(self._filename).bind(),
 | 
				
			||||||
            "ak_user_by": BaseEvaluator.expr_user_by,
 | 
					            "ak_user_by": BaseEvaluator.expr_user_by,
 | 
				
			||||||
            "ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
 | 
					            "ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
 | 
				
			||||||
 | 
					            "ak_create_jwt": self.expr_create_jwt,
 | 
				
			||||||
            "ip_address": ip_address,
 | 
					            "ip_address": ip_address,
 | 
				
			||||||
            "ip_network": ip_network,
 | 
					            "ip_network": ip_network,
 | 
				
			||||||
            "list_flatten": BaseEvaluator.expr_flatten,
 | 
					            "list_flatten": BaseEvaluator.expr_flatten,
 | 
				
			||||||
@ -182,6 +188,36 @@ class BaseEvaluator:
 | 
				
			|||||||
        proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
 | 
					        proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
 | 
				
			||||||
        return proc.profiling_wrapper()
 | 
					        return proc.profiling_wrapper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expr_create_jwt(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        user: User,
 | 
				
			||||||
 | 
					        provider: OAuth2Provider | str,
 | 
				
			||||||
 | 
					        scopes: list[str],
 | 
				
			||||||
 | 
					        validity: str = "seconds=60",
 | 
				
			||||||
 | 
					    ) -> str | None:
 | 
				
			||||||
 | 
					        """Issue a JWT for a given provider"""
 | 
				
			||||||
 | 
					        request: HttpRequest = self._context.get("http_request")
 | 
				
			||||||
 | 
					        if not request:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        if not isinstance(provider, OAuth2Provider):
 | 
				
			||||||
 | 
					            provider = OAuth2Provider.objects.get(name=provider)
 | 
				
			||||||
 | 
					        session = None
 | 
				
			||||||
 | 
					        if hasattr(request, "session") and request.session.session_key:
 | 
				
			||||||
 | 
					            session = AuthenticatedSession.objects.filter(
 | 
				
			||||||
 | 
					                session_key=request.session.session_key
 | 
				
			||||||
 | 
					            ).first()
 | 
				
			||||||
 | 
					        access_token = AccessToken(
 | 
				
			||||||
 | 
					            provider=provider,
 | 
				
			||||||
 | 
					            user=user,
 | 
				
			||||||
 | 
					            expires=now() + timedelta_from_string(validity),
 | 
				
			||||||
 | 
					            scope=scopes,
 | 
				
			||||||
 | 
					            auth_time=now(),
 | 
				
			||||||
 | 
					            session=session,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        access_token.id_token = IDToken.new(provider, access_token, request)
 | 
				
			||||||
 | 
					        access_token.save()
 | 
				
			||||||
 | 
					        return access_token.token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def wrap_expression(self, expression: str) -> str:
 | 
					    def wrap_expression(self, expression: str) -> str:
 | 
				
			||||||
        """Wrap expression in a function, call it, and save the result as `result`"""
 | 
					        """Wrap expression in a function, call it, and save the result as `result`"""
 | 
				
			||||||
        handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
 | 
					        handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,15 @@
 | 
				
			|||||||
"""Test Evaluator base functions"""
 | 
					"""Test Evaluator base functions"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import RequestFactory, TestCase
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from jwt import decode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user
 | 
					from authentik.blueprints.tests import apply_blueprint
 | 
				
			||||||
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
 | 
				
			||||||
from authentik.events.models import Event
 | 
					from authentik.events.models import Event
 | 
				
			||||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
					from authentik.lib.expression.evaluator import BaseEvaluator
 | 
				
			||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
 | 
					from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestEvaluator(TestCase):
 | 
					class TestEvaluator(TestCase):
 | 
				
			||||||
@ -41,3 +45,35 @@ class TestEvaluator(TestCase):
 | 
				
			|||||||
        event = Event.objects.filter(action="custom_foo").first()
 | 
					        event = Event.objects.filter(action="custom_foo").first()
 | 
				
			||||||
        self.assertIsNotNone(event)
 | 
					        self.assertIsNotNone(event)
 | 
				
			||||||
        self.assertEqual(event.context, {"bar": "baz", "foo": "bar"})
 | 
					        self.assertEqual(event.context, {"bar": "baz", "foo": "bar"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @apply_blueprint("system/providers-oauth2.yaml")
 | 
				
			||||||
 | 
					    def test_expr_create_jwt(self):
 | 
				
			||||||
 | 
					        """Test expr_create_jwt"""
 | 
				
			||||||
 | 
					        rf = RequestFactory()
 | 
				
			||||||
 | 
					        user = create_test_user()
 | 
				
			||||||
 | 
					        provider = OAuth2Provider.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            authorization_flow=create_test_flow(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        provider.property_mappings.set(
 | 
				
			||||||
 | 
					            ScopeMapping.objects.filter(
 | 
				
			||||||
 | 
					                managed__in=[
 | 
				
			||||||
 | 
					                    "goauthentik.io/providers/oauth2/scope-openid",
 | 
				
			||||||
 | 
					                    "goauthentik.io/providers/oauth2/scope-email",
 | 
				
			||||||
 | 
					                    "goauthentik.io/providers/oauth2/scope-profile",
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        evaluator = BaseEvaluator(generate_id())
 | 
				
			||||||
 | 
					        evaluator._context = {
 | 
				
			||||||
 | 
					            "http_request": rf.get(reverse("authentik_core:root-redirect")),
 | 
				
			||||||
 | 
					            "user": user,
 | 
				
			||||||
 | 
					            "provider": provider.name,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        jwt = evaluator.evaluate(
 | 
				
			||||||
 | 
					            "return ak_create_jwt(user, provider, ['openid', 'email', 'profile'])"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        decoded = decode(
 | 
				
			||||||
 | 
					            jwt, provider.client_secret, algorithms=["HS256"], audience=provider.client_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(decoded["preferred_username"], user.username)
 | 
				
			||||||
 | 
				
			|||||||
@ -207,7 +207,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
                "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
 | 
					                "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
 | 
				
			||||||
                "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
					                "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
				
			||||||
                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
 | 
					                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
 | 
				
			||||||
                "app.kubernetes.io/version": get_version(),
 | 
					                "app.kubernetes.io/version": get_version().replace("+", "-"),
 | 
				
			||||||
                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
 | 
					                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
 | 
				
			||||||
                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
 | 
					                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
 | 
				
			||||||
                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
					                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
				
			||||||
 | 
				
			|||||||
@ -94,7 +94,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        meta = self.get_object_meta(name=self.name)
 | 
					        meta = self.get_object_meta(name=self.name)
 | 
				
			||||||
        image_name = self.controller.get_container_image()
 | 
					        image_name = self.controller.get_container_image()
 | 
				
			||||||
        image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
 | 
					        image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
 | 
				
			||||||
        version = get_full_version()
 | 
					        version = get_full_version().replace("+", "-")
 | 
				
			||||||
        return V1Deployment(
 | 
					        return V1Deployment(
 | 
				
			||||||
            metadata=meta,
 | 
					            metadata=meta,
 | 
				
			||||||
            spec=V1DeploymentSpec(
 | 
					            spec=V1DeploymentSpec(
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ if TYPE_CHECKING:
 | 
				
			|||||||
    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class PrometheusServiceMonitorSpecEndpoint:
 | 
					class PrometheusServiceMonitorSpecEndpoint:
 | 
				
			||||||
    """Prometheus ServiceMonitor endpoint spec"""
 | 
					    """Prometheus ServiceMonitor endpoint spec"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,14 +21,14 @@ class PrometheusServiceMonitorSpecEndpoint:
 | 
				
			|||||||
    path: str = field(default="/metrics")
 | 
					    path: str = field(default="/metrics")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class PrometheusServiceMonitorSpecSelector:
 | 
					class PrometheusServiceMonitorSpecSelector:
 | 
				
			||||||
    """Prometheus ServiceMonitor selector spec"""
 | 
					    """Prometheus ServiceMonitor selector spec"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    matchLabels: dict
 | 
					    matchLabels: dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class PrometheusServiceMonitorSpec:
 | 
					class PrometheusServiceMonitorSpec:
 | 
				
			||||||
    """Prometheus ServiceMonitor spec"""
 | 
					    """Prometheus ServiceMonitor spec"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,7 +37,7 @@ class PrometheusServiceMonitorSpec:
 | 
				
			|||||||
    selector: PrometheusServiceMonitorSpecSelector
 | 
					    selector: PrometheusServiceMonitorSpecSelector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class PrometheusServiceMonitorMetadata:
 | 
					class PrometheusServiceMonitorMetadata:
 | 
				
			||||||
    """Prometheus ServiceMonitor metadata"""
 | 
					    """Prometheus ServiceMonitor metadata"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -46,7 +46,7 @@ class PrometheusServiceMonitorMetadata:
 | 
				
			|||||||
    labels: dict = field(default_factory=dict)
 | 
					    labels: dict = field(default_factory=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class PrometheusServiceMonitor:
 | 
					class PrometheusServiceMonitor:
 | 
				
			||||||
    """Prometheus ServiceMonitor"""
 | 
					    """Prometheus ServiceMonitor"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            "authentik_policies_reputation",
 | 
				
			||||||
 | 
					            "0007_reputation_authentik_p_identif_9434d7_idx_and_more",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="reputation",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_p_expires_da493f_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="reputation",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_p_expirin_2ab34f_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="reputation",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_p_expirin_2a8ec7_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -96,7 +96,7 @@ class Reputation(ExpiringModel, SerializerModel):
 | 
				
			|||||||
        verbose_name = _("Reputation Score")
 | 
					        verbose_name = _("Reputation Score")
 | 
				
			||||||
        verbose_name_plural = _("Reputation Scores")
 | 
					        verbose_name_plural = _("Reputation Scores")
 | 
				
			||||||
        unique_together = ("identifier", "ip")
 | 
					        unique_together = ("identifier", "ip")
 | 
				
			||||||
        indexes = [
 | 
					        indexes = ExpiringModel.Meta.indexes + [
 | 
				
			||||||
            models.Index(fields=["identifier"]),
 | 
					            models.Index(fields=["identifier"]),
 | 
				
			||||||
            models.Index(fields=["ip"]),
 | 
					            models.Index(fields=["ip"]),
 | 
				
			||||||
            models.Index(fields=["ip", "identifier"]),
 | 
					            models.Index(fields=["ip", "identifier"]),
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
 | 
				
			||||||
 | 
					        ("authentik_providers_oauth2", "0026_alter_accesstoken_session_and_more"),
 | 
				
			||||||
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="accesstoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_p_expires_9f24a5_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="accesstoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_p_expirin_2d9205_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="accesstoken",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_p_expirin_c74005_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authorizationcode",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_p_expires_f594b2_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authorizationcode",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_p_expirin_6a5e2c_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="authorizationcode",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_p_expirin_c0f353_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="devicetoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_p_expires_961437_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="devicetoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_p_expirin_4fd278_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="devicetoken",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_p_expirin_cd6b1c_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="refreshtoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_p_expires_c479a7_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="refreshtoken",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_p_expirin_d4d17f_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="refreshtoken",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_p_expirin_acb4a5_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -425,6 +425,7 @@ class AuthorizationCode(SerializerModel, ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Authorization Code")
 | 
					        verbose_name = _("Authorization Code")
 | 
				
			||||||
        verbose_name_plural = _("Authorization Codes")
 | 
					        verbose_name_plural = _("Authorization Codes")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Authorization code for {self.provider_id} for user {self.user_id}"
 | 
					        return f"Authorization code for {self.provider_id} for user {self.user_id}"
 | 
				
			||||||
@ -453,7 +454,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
    _id_token = models.TextField()
 | 
					    _id_token = models.TextField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        indexes = [
 | 
					        indexes = ExpiringModel.Meta.indexes + [
 | 
				
			||||||
            HashIndex(fields=["token"]),
 | 
					            HashIndex(fields=["token"]),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        verbose_name = _("OAuth2 Access Token")
 | 
					        verbose_name = _("OAuth2 Access Token")
 | 
				
			||||||
@ -504,7 +505,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        indexes = [
 | 
					        indexes = ExpiringModel.Meta.indexes + [
 | 
				
			||||||
            HashIndex(fields=["token"]),
 | 
					            HashIndex(fields=["token"]),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        verbose_name = _("OAuth2 Refresh Token")
 | 
					        verbose_name = _("OAuth2 Refresh Token")
 | 
				
			||||||
@ -556,6 +557,7 @@ class DeviceToken(ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Device Token")
 | 
					        verbose_name = _("Device Token")
 | 
				
			||||||
        verbose_name_plural = _("Device Tokens")
 | 
					        verbose_name_plural = _("Device Tokens")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Device Token for {self.provider_id}"
 | 
					        return f"Device Token for {self.provider_id}"
 | 
				
			||||||
 | 
				
			|||||||
@ -49,7 +49,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
				
			|||||||
                kwargs={
 | 
					                kwargs={
 | 
				
			||||||
                    "flow_slug": self.device_flow.slug,
 | 
					                    "flow_slug": self.device_flow.slug,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            ),
 | 
					            )
 | 
				
			||||||
 | 
					            + "?"
 | 
				
			||||||
 | 
					            + urlencode({"inspector": "available"}),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_device_init_post(self):
 | 
					    def test_device_init_post(self):
 | 
				
			||||||
@ -63,7 +65,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
				
			|||||||
                kwargs={
 | 
					                kwargs={
 | 
				
			||||||
                    "flow_slug": self.device_flow.slug,
 | 
					                    "flow_slug": self.device_flow.slug,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            ),
 | 
					            )
 | 
				
			||||||
 | 
					            + "?"
 | 
				
			||||||
 | 
					            + urlencode({"inspector": "available"}),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        res = self.api_client.get(
 | 
					        res = self.api_client.get(
 | 
				
			||||||
            reverse(
 | 
					            reverse(
 | 
				
			||||||
@ -118,7 +122,9 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
				
			|||||||
                    kwargs={
 | 
					                    kwargs={
 | 
				
			||||||
                        "flow_slug": provider.authorization_flow.slug,
 | 
					                        "flow_slug": provider.authorization_flow.slug,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                ),
 | 
					                )
 | 
				
			||||||
 | 
					                + "?"
 | 
				
			||||||
 | 
					                + urlencode({"inspector": "available"}),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,7 +156,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            + "?"
 | 
					            + "?"
 | 
				
			||||||
            + urlencode({QS_KEY_CODE: token.user_code}),
 | 
					            + urlencode({QS_KEY_CODE: token.user_code, "inspector": "available"}),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_device_init_denied(self):
 | 
					    def test_device_init_denied(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ if TYPE_CHECKING:
 | 
				
			|||||||
    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class TraefikMiddlewareSpecForwardAuth:
 | 
					class TraefikMiddlewareSpecForwardAuth:
 | 
				
			||||||
    """traefik middleware forwardAuth spec"""
 | 
					    """traefik middleware forwardAuth spec"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,14 +28,14 @@ class TraefikMiddlewareSpecForwardAuth:
 | 
				
			|||||||
    trustForwardHeader: bool = field(default=True)
 | 
					    trustForwardHeader: bool = field(default=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class TraefikMiddlewareSpec:
 | 
					class TraefikMiddlewareSpec:
 | 
				
			||||||
    """Traefik middleware spec"""
 | 
					    """Traefik middleware spec"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    forwardAuth: TraefikMiddlewareSpecForwardAuth
 | 
					    forwardAuth: TraefikMiddlewareSpecForwardAuth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class TraefikMiddlewareMetadata:
 | 
					class TraefikMiddlewareMetadata:
 | 
				
			||||||
    """Traefik Middleware metadata"""
 | 
					    """Traefik Middleware metadata"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,7 +44,7 @@ class TraefikMiddlewareMetadata:
 | 
				
			|||||||
    labels: dict = field(default_factory=dict)
 | 
					    labels: dict = field(default_factory=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass(slots=True)
 | 
				
			||||||
class TraefikMiddleware:
 | 
					class TraefikMiddleware:
 | 
				
			||||||
    """Traefik Middleware"""
 | 
					    """Traefik Middleware"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ from rest_framework.decorators import action
 | 
				
			|||||||
from rest_framework.fields import CharField, FileField, SerializerMethodField
 | 
					from rest_framework.fields import CharField, FileField, SerializerMethodField
 | 
				
			||||||
from rest_framework.parsers import MultiPartParser
 | 
					from rest_framework.parsers import MultiPartParser
 | 
				
			||||||
from rest_framework.permissions import AllowAny
 | 
					from rest_framework.permissions import AllowAny
 | 
				
			||||||
 | 
					from rest_framework.renderers import BaseRenderer, JSONRenderer
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError
 | 
					from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError
 | 
				
			||||||
@ -38,6 +39,16 @@ from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_
 | 
				
			|||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RawXMLDataRenderer(BaseRenderer):
 | 
				
			||||||
 | 
					    """Renderer to allow application/xml as value for 'Accept' in the metadata endpoint."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    media_type = "application/xml"
 | 
				
			||||||
 | 
					    format = "xml"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render(self, data, accepted_media_type=None, renderer_context=None):
 | 
				
			||||||
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SAMLProviderSerializer(ProviderSerializer):
 | 
					class SAMLProviderSerializer(ProviderSerializer):
 | 
				
			||||||
    """SAMLProvider Serializer"""
 | 
					    """SAMLProvider Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -54,9 +65,23 @@ class SAMLProviderSerializer(ProviderSerializer):
 | 
				
			|||||||
        if "request" not in self._context:
 | 
					        if "request" not in self._context:
 | 
				
			||||||
            return ""
 | 
					            return ""
 | 
				
			||||||
        request: HttpRequest = self._context["request"]._request
 | 
					        request: HttpRequest = self._context["request"]._request
 | 
				
			||||||
        return request.build_absolute_uri(
 | 
					        try:
 | 
				
			||||||
            reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk}) + "?download"
 | 
					            return request.build_absolute_uri(
 | 
				
			||||||
        )
 | 
					                reverse(
 | 
				
			||||||
 | 
					                    "authentik_providers_saml:metadata-download",
 | 
				
			||||||
 | 
					                    kwargs={"application_slug": instance.application.slug},
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        except Provider.application.RelatedObjectDoesNotExist:
 | 
				
			||||||
 | 
					            return request.build_absolute_uri(
 | 
				
			||||||
 | 
					                reverse(
 | 
				
			||||||
 | 
					                    "authentik_api:samlprovider-metadata",
 | 
				
			||||||
 | 
					                    kwargs={
 | 
				
			||||||
 | 
					                        "pk": instance.pk,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                + "?download"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_url_sso_post(self, instance: SAMLProvider) -> str:
 | 
					    def get_url_sso_post(self, instance: SAMLProvider) -> str:
 | 
				
			||||||
        """Get SSO Post URL"""
 | 
					        """Get SSO Post URL"""
 | 
				
			||||||
@ -224,9 +249,21 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
                description="Optionally force the metadata to only include one binding.",
 | 
					                description="Optionally force the metadata to only include one binding.",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            # Explicitly excluded, because otherwise spectacular automatically
 | 
				
			||||||
 | 
					            # add it when using multiple renderer_classes
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                name="format",
 | 
				
			||||||
 | 
					                exclude=True,
 | 
				
			||||||
 | 
					                required=False,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    @action(methods=["GET"], detail=True, permission_classes=[AllowAny])
 | 
					    @action(
 | 
				
			||||||
 | 
					        methods=["GET"],
 | 
				
			||||||
 | 
					        detail=True,
 | 
				
			||||||
 | 
					        permission_classes=[AllowAny],
 | 
				
			||||||
 | 
					        renderer_classes=[JSONRenderer, RawXMLDataRenderer],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def metadata(self, request: Request, pk: int) -> Response:
 | 
					    def metadata(self, request: Request, pk: int) -> Response:
 | 
				
			||||||
        """Return metadata as XML string"""
 | 
					        """Return metadata as XML string"""
 | 
				
			||||||
        # We don't use self.get_object() on purpose as this view is un-authenticated
 | 
					        # We don't use self.get_object() on purpose as this view is un-authenticated
 | 
				
			||||||
@ -244,9 +281,9 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
                    f'attachment; filename="{provider.name}_authentik_meta.xml"'
 | 
					                    f'attachment; filename="{provider.name}_authentik_meta.xml"'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                return response
 | 
					                return response
 | 
				
			||||||
            return Response({"metadata": metadata})
 | 
					            return Response({"metadata": metadata}, content_type="application/json")
 | 
				
			||||||
        except Provider.application.RelatedObjectDoesNotExist:
 | 
					        except Provider.application.RelatedObjectDoesNotExist:
 | 
				
			||||||
            return Response({"metadata": ""})
 | 
					            return Response({"metadata": ""}, content_type="application/json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @permission_required(
 | 
					    @permission_required(
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
 | 
				
			|||||||
@ -256,7 +256,7 @@ class AssertionProcessor:
 | 
				
			|||||||
        assertion.attrib["IssueInstant"] = self._issue_instant
 | 
					        assertion.attrib["IssueInstant"] = self._issue_instant
 | 
				
			||||||
        assertion.append(self.get_issuer())
 | 
					        assertion.append(self.get_issuer())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.provider.signing_kp:
 | 
					        if self.provider.signing_kp and self.provider.sign_assertion:
 | 
				
			||||||
            sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
 | 
					            sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
 | 
				
			||||||
                self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
 | 
					                self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@ -295,6 +295,18 @@ class AssertionProcessor:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response.append(self.get_issuer())
 | 
					        response.append(self.get_issuer())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.provider.signing_kp and self.provider.sign_response:
 | 
				
			||||||
 | 
					            sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
 | 
				
			||||||
 | 
					                self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            signature = xmlsec.template.create(
 | 
				
			||||||
 | 
					                response,
 | 
				
			||||||
 | 
					                xmlsec.constants.TransformExclC14N,
 | 
				
			||||||
 | 
					                sign_algorithm_transform,
 | 
				
			||||||
 | 
					                ns=xmlsec.constants.DSigNs,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            response.append(signature)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        status = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
 | 
					        status = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
 | 
				
			||||||
        status_code = SubElement(status, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
 | 
					        status_code = SubElement(status, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
 | 
				
			||||||
        status_code.attrib["Value"] = "urn:oasis:names:tc:SAML:2.0:status:Success"
 | 
					        status_code.attrib["Value"] = "urn:oasis:names:tc:SAML:2.0:status:Success"
 | 
				
			||||||
 | 
				
			|||||||
@ -104,6 +104,22 @@ class TestSAMLProviderAPI(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(200, response.status_code)
 | 
					        self.assertEqual(200, response.status_code)
 | 
				
			||||||
        self.assertIn("Content-Disposition", response)
 | 
					        self.assertIn("Content-Disposition", response)
 | 
				
			||||||
 | 
					        # Test download with Accept: application/xml
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
 | 
				
			||||||
 | 
					            + "?download",
 | 
				
			||||||
 | 
					            HTTP_ACCEPT="application/xml",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(200, response.status_code)
 | 
				
			||||||
 | 
					        self.assertIn("Content-Disposition", response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
 | 
				
			||||||
 | 
					            + "?download",
 | 
				
			||||||
 | 
					            HTTP_ACCEPT="application/xml;charset=UTF-8",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(200, response.status_code)
 | 
				
			||||||
 | 
					        self.assertIn("Content-Disposition", response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_metadata_invalid(self):
 | 
					    def test_metadata_invalid(self):
 | 
				
			||||||
        """Test metadata export (invalid)"""
 | 
					        """Test metadata export (invalid)"""
 | 
				
			||||||
@ -121,6 +137,11 @@ class TestSAMLProviderAPI(APITestCase):
 | 
				
			|||||||
            reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}),
 | 
					            reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(404, response.status_code)
 | 
					        self.assertEqual(404, response.status_code)
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
 | 
				
			||||||
 | 
					            HTTP_ACCEPT="application/invalid-mime-type",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(406, response.status_code)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_import_success(self):
 | 
					    def test_import_success(self):
 | 
				
			||||||
        """Test metadata import (success case)"""
 | 
					        """Test metadata import (success case)"""
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from base64 import b64encode
 | 
					from base64 import b64encode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from defusedxml.lxml import fromstring
 | 
				
			||||||
from django.http.request import QueryDict
 | 
					from django.http.request import QueryDict
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					from lxml import etree  # nosec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.blueprints.tests import apply_blueprint
 | 
					from authentik.blueprints.tests import apply_blueprint
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
 | 
				
			||||||
@ -11,12 +13,14 @@ from authentik.crypto.models import CertificateKeyPair
 | 
				
			|||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
from authentik.lib.tests.utils import get_request
 | 
					from authentik.lib.tests.utils import get_request
 | 
				
			||||||
 | 
					from authentik.lib.xml import lxml_from_string
 | 
				
			||||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
 | 
					from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
 | 
				
			||||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
 | 
					from authentik.providers.saml.processors.assertion import AssertionProcessor
 | 
				
			||||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
 | 
					from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
 | 
				
			||||||
from authentik.sources.saml.exceptions import MismatchedRequestID
 | 
					from authentik.sources.saml.exceptions import MismatchedRequestID
 | 
				
			||||||
from authentik.sources.saml.models import SAMLSource
 | 
					from authentik.sources.saml.models import SAMLSource
 | 
				
			||||||
from authentik.sources.saml.processors.constants import (
 | 
					from authentik.sources.saml.processors.constants import (
 | 
				
			||||||
 | 
					    NS_MAP,
 | 
				
			||||||
    SAML_BINDING_REDIRECT,
 | 
					    SAML_BINDING_REDIRECT,
 | 
				
			||||||
    SAML_NAME_ID_FORMAT_EMAIL,
 | 
					    SAML_NAME_ID_FORMAT_EMAIL,
 | 
				
			||||||
    SAML_NAME_ID_FORMAT_UNSPECIFIED,
 | 
					    SAML_NAME_ID_FORMAT_UNSPECIFIED,
 | 
				
			||||||
@ -185,6 +189,19 @@ class TestAuthNRequest(TestCase):
 | 
				
			|||||||
        self.assertEqual(response.count(response_proc._assertion_id), 2)
 | 
					        self.assertEqual(response.count(response_proc._assertion_id), 2)
 | 
				
			||||||
        self.assertEqual(response.count(response_proc._response_id), 2)
 | 
					        self.assertEqual(response.count(response_proc._response_id), 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        schema = etree.XMLSchema(
 | 
				
			||||||
 | 
					            etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser())  # nosec
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertTrue(schema.validate(lxml_from_string(response)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response_xml = fromstring(response)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Now parse the response (source)
 | 
					        # Now parse the response (source)
 | 
				
			||||||
        http_request.POST = QueryDict(mutable=True)
 | 
					        http_request.POST = QueryDict(mutable=True)
 | 
				
			||||||
        http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
 | 
					        http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ from django.contrib.auth.models import Permission
 | 
				
			|||||||
from django.db.models import QuerySet
 | 
					from django.db.models import QuerySet
 | 
				
			||||||
from django_filters.filters import ModelChoiceFilter
 | 
					from django_filters.filters import ModelChoiceFilter
 | 
				
			||||||
from django_filters.filterset import FilterSet
 | 
					from django_filters.filterset import FilterSet
 | 
				
			||||||
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
from rest_framework.exceptions import ValidationError
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
from rest_framework.fields import (
 | 
					from rest_framework.fields import (
 | 
				
			||||||
    CharField,
 | 
					    CharField,
 | 
				
			||||||
@ -13,6 +14,8 @@ from rest_framework.fields import (
 | 
				
			|||||||
    ReadOnlyField,
 | 
					    ReadOnlyField,
 | 
				
			||||||
    SerializerMethodField,
 | 
					    SerializerMethodField,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from rest_framework.filters import OrderingFilter, SearchFilter
 | 
				
			||||||
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
					from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
 | 
					from authentik.core.api.utils import ModelSerializer, PassiveSerializer
 | 
				
			||||||
@ -92,7 +95,9 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
 | 
				
			|||||||
    queryset = Permission.objects.none()
 | 
					    queryset = Permission.objects.none()
 | 
				
			||||||
    serializer_class = PermissionSerializer
 | 
					    serializer_class = PermissionSerializer
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
				
			||||||
    filterset_class = PermissionFilter
 | 
					    filterset_class = PermissionFilter
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
    search_fields = [
 | 
					    search_fields = [
 | 
				
			||||||
        "codename",
 | 
					        "codename",
 | 
				
			||||||
        "content_type__model",
 | 
					        "content_type__model",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,15 @@
 | 
				
			|||||||
"""RBAC API Filter"""
 | 
					"""RBAC API Filter"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db.models import QuerySet
 | 
					from django.db.models import QuerySet
 | 
				
			||||||
 | 
					from django_filters.rest_framework import DjangoFilterBackend
 | 
				
			||||||
 | 
					from rest_framework.authentication import get_authorization_header
 | 
				
			||||||
from rest_framework.exceptions import PermissionDenied
 | 
					from rest_framework.exceptions import PermissionDenied
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
					from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.api.authentication import validate_auth
 | 
				
			||||||
from authentik.core.models import UserTypes
 | 
					from authentik.core.models import UserTypes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,7 +17,7 @@ class ObjectFilter(ObjectPermissionsFilter):
 | 
				
			|||||||
    """Object permission filter that grants global permission higher priority than
 | 
					    """Object permission filter that grants global permission higher priority than
 | 
				
			||||||
    per-object permissions"""
 | 
					    per-object permissions"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
 | 
					    def filter_queryset(self, request: Request, queryset: QuerySet, view: APIView) -> QuerySet:
 | 
				
			||||||
        permission = self.perm_format % {
 | 
					        permission = self.perm_format % {
 | 
				
			||||||
            "app_label": queryset.model._meta.app_label,
 | 
					            "app_label": queryset.model._meta.app_label,
 | 
				
			||||||
            "model_name": queryset.model._meta.model_name,
 | 
					            "model_name": queryset.model._meta.model_name,
 | 
				
			||||||
@ -21,6 +26,9 @@ class ObjectFilter(ObjectPermissionsFilter):
 | 
				
			|||||||
        # per-object permissions
 | 
					        # per-object permissions
 | 
				
			||||||
        if request.user.has_perm(permission):
 | 
					        if request.user.has_perm(permission):
 | 
				
			||||||
            return queryset
 | 
					            return queryset
 | 
				
			||||||
 | 
					        # User does not have permissions, but we have an owner field defined, so filter by that
 | 
				
			||||||
 | 
					        if owner_field := getattr(view, "owner_field", None):
 | 
				
			||||||
 | 
					            return queryset.filter(**{owner_field: request.user})
 | 
				
			||||||
        queryset = super().filter_queryset(request, queryset, view)
 | 
					        queryset = super().filter_queryset(request, queryset, view)
 | 
				
			||||||
        # Outposts (which are the only objects using internal service accounts)
 | 
					        # Outposts (which are the only objects using internal service accounts)
 | 
				
			||||||
        # except requests to return an empty list when they have no objects
 | 
					        # except requests to return an empty list when they have no objects
 | 
				
			||||||
@ -32,3 +40,17 @@ class ObjectFilter(ObjectPermissionsFilter):
 | 
				
			|||||||
            # and also no object permissions assigned (directly or via role)
 | 
					            # and also no object permissions assigned (directly or via role)
 | 
				
			||||||
            raise PermissionDenied()
 | 
					            raise PermissionDenied()
 | 
				
			||||||
        return queryset
 | 
					        return queryset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SecretKeyFilter(DjangoFilterBackend):
 | 
				
			||||||
 | 
					    """Allow access to all objects when authenticated with secret key as token.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Replaces both DjangoFilterBackend and ObjectFilter"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
 | 
				
			||||||
 | 
					        auth_header = get_authorization_header(request)
 | 
				
			||||||
 | 
					        token = validate_auth(auth_header)
 | 
				
			||||||
 | 
					        if token and token == settings.SECRET_KEY:
 | 
				
			||||||
 | 
					            return queryset
 | 
				
			||||||
 | 
					        queryset = ObjectFilter().filter_queryset(request, queryset, view)
 | 
				
			||||||
 | 
					        return super().filter_queryset(request, queryset, view)
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,17 @@ class ObjectPermissions(DjangoObjectPermissions):
 | 
				
			|||||||
        lookup = getattr(view, "lookup_url_kwarg", None) or getattr(view, "lookup_field", None)
 | 
					        lookup = getattr(view, "lookup_url_kwarg", None) or getattr(view, "lookup_field", None)
 | 
				
			||||||
        if lookup and lookup in view.kwargs:
 | 
					        if lookup and lookup in view.kwargs:
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					        # Legacy behaviour:
 | 
				
			||||||
 | 
					        # Allow creation of objects even without explicit permission
 | 
				
			||||||
 | 
					        queryset = self._queryset(view)
 | 
				
			||||||
 | 
					        required_perms = self.get_required_permissions(request.method, queryset.model)
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            len(required_perms) == 1
 | 
				
			||||||
 | 
					            and f"{queryset.model._meta.app_label}.add_{queryset.model._meta.model_name}"
 | 
				
			||||||
 | 
					            in required_perms
 | 
				
			||||||
 | 
					            and getattr(view, "rbac_allow_create_without_perm", False)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
        return super().has_permission(request, view)
 | 
					        return super().has_permission(request, view)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def has_object_permission(self, request: Request, view, obj: Model) -> bool:
 | 
					    def has_object_permission(self, request: Request, view, obj: Model) -> bool:
 | 
				
			||||||
@ -24,6 +35,10 @@ class ObjectPermissions(DjangoObjectPermissions):
 | 
				
			|||||||
        # Rank global permissions higher than per-object permissions
 | 
					        # Rank global permissions higher than per-object permissions
 | 
				
			||||||
        if request.user.has_perms(perms):
 | 
					        if request.user.has_perms(perms):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					        # Allow access for owners if configured
 | 
				
			||||||
 | 
					        if owner_field := getattr(view, "owner_field", None):
 | 
				
			||||||
 | 
					            if getattr(obj, owner_field) == request.user:
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
        return super().has_object_permission(request, view, obj)
 | 
					        return super().has_object_permission(request, view, obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@ from celery.signals import (
 | 
				
			|||||||
    task_prerun,
 | 
					    task_prerun,
 | 
				
			||||||
    worker_ready,
 | 
					    worker_ready,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from celery.worker.control import inspect_command
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import ProgrammingError
 | 
					from django.db import ProgrammingError
 | 
				
			||||||
from django_tenants.utils import get_public_schema_name
 | 
					from django_tenants.utils import get_public_schema_name
 | 
				
			||||||
@ -25,6 +26,7 @@ from structlog.contextvars import STRUCTLOG_KEY_PREFIX
 | 
				
			|||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
 | 
					from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik import get_full_version
 | 
				
			||||||
from authentik.lib.sentry import before_send
 | 
					from authentik.lib.sentry import before_send
 | 
				
			||||||
from authentik.lib.utils.errors import exception_to_string
 | 
					from authentik.lib.utils.errors import exception_to_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -159,6 +161,12 @@ class LivenessProbe(bootsteps.StartStopStep):
 | 
				
			|||||||
        HEARTBEAT_FILE.touch()
 | 
					        HEARTBEAT_FILE.touch()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@inspect_command(default_timeout=0.2)
 | 
				
			||||||
 | 
					def ping(state, **kwargs):
 | 
				
			||||||
 | 
					    """Ping worker(s)."""
 | 
				
			||||||
 | 
					    return {"ok": "pong", "version": get_full_version()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CELERY_APP.config_from_object(settings.CELERY)
 | 
					CELERY_APP.config_from_object(settings.CELERY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Load task modules from all registered Django app configs.
 | 
					# Load task modules from all registered Django app configs.
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,8 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if kwargs.get("randomly_seed", None):
 | 
					        if kwargs.get("randomly_seed", None):
 | 
				
			||||||
            self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
 | 
					            self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
 | 
				
			||||||
 | 
					        if kwargs.get("no_capture"):
 | 
				
			||||||
 | 
					            self.args.append("-s")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        settings.TEST = True
 | 
					        settings.TEST = True
 | 
				
			||||||
        settings.CELERY["task_always_eager"] = True
 | 
					        settings.CELERY["task_always_eager"] = True
 | 
				
			||||||
@ -56,6 +58,10 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover
 | 
				
			|||||||
    def add_arguments(cls, parser: ArgumentParser):
 | 
					    def add_arguments(cls, parser: ArgumentParser):
 | 
				
			||||||
        """Add more pytest-specific arguments"""
 | 
					        """Add more pytest-specific arguments"""
 | 
				
			||||||
        DiscoverRunner.add_arguments(parser)
 | 
					        DiscoverRunner.add_arguments(parser)
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--no-capture",
 | 
				
			||||||
 | 
					            action="store_true",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        parser.add_argument(
 | 
					        parser.add_argument(
 | 
				
			||||||
            "--randomly-seed",
 | 
					            "--randomly-seed",
 | 
				
			||||||
            type=int,
 | 
					            type=int,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,7 @@
 | 
				
			|||||||
"""Kerberos Source Serializer"""
 | 
					"""Kerberos Source Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
 | 
					 | 
				
			||||||
from authentik.core.api.sources import (
 | 
					from authentik.core.api.sources import (
 | 
				
			||||||
    GroupSourceConnectionSerializer,
 | 
					    GroupSourceConnectionSerializer,
 | 
				
			||||||
    GroupSourceConnectionViewSet,
 | 
					    GroupSourceConnectionViewSet,
 | 
				
			||||||
@ -32,9 +29,8 @@ class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    serializer_class = UserKerberosSourceConnectionSerializer
 | 
					    serializer_class = UserKerberosSourceConnectionSerializer
 | 
				
			||||||
    filterset_fields = ["source__slug"]
 | 
					    filterset_fields = ["source__slug"]
 | 
				
			||||||
    search_fields = ["source__slug"]
 | 
					    search_fields = ["source__slug"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
    ordering = ["source__slug"]
 | 
					    ordering = ["source__slug"]
 | 
				
			||||||
 | 
					    owner_field = "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
 | 
					class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
 | 
				
			||||||
 | 
				
			|||||||
@ -28,17 +28,19 @@ class KerberosBackend(InbuiltBackend):
 | 
				
			|||||||
        if "@" in username:
 | 
					        if "@" in username:
 | 
				
			||||||
            username, realm = username.rsplit("@", 1)
 | 
					            username, realm = username.rsplit("@", 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user, source = self.auth_user(username, realm, **kwargs)
 | 
					        user, source = self.auth_user(request, username, realm, **kwargs)
 | 
				
			||||||
        if user:
 | 
					        if user:
 | 
				
			||||||
            self.set_method("kerberos", request, source=source)
 | 
					            self.set_method("kerberos", request, source=source)
 | 
				
			||||||
            return user
 | 
					            return user
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def auth_user(
 | 
					    def auth_user(
 | 
				
			||||||
        self, username: str, realm: str | None, password: str, **filters
 | 
					        self, request: HttpRequest, username: str, realm: str | None, password: str, **filters
 | 
				
			||||||
    ) -> tuple[User | None, KerberosSource | None]:
 | 
					    ) -> tuple[User | None, KerberosSource | None]:
 | 
				
			||||||
        sources = KerberosSource.objects.filter(enabled=True)
 | 
					        sources = KerberosSource.objects.filter(enabled=True)
 | 
				
			||||||
        user = User.objects.filter(usersourceconnection__source__in=sources, **filters).first()
 | 
					        user = User.objects.filter(
 | 
				
			||||||
 | 
					            usersourceconnection__source__in=sources, username=username, **filters
 | 
				
			||||||
 | 
					        ).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if user is not None:
 | 
					        if user is not None:
 | 
				
			||||||
            # User found, let's get its connections for the sources that are available
 | 
					            # User found, let's get its connections for the sources that are available
 | 
				
			||||||
@ -74,10 +76,10 @@ class KerberosBackend(InbuiltBackend):
 | 
				
			|||||||
                        user=user_source_connection.user,
 | 
					                        user=user_source_connection.user,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    user_source_connection.user.set_password(
 | 
					                    user_source_connection.user.set_password(
 | 
				
			||||||
                        password, sender=user_source_connection.source
 | 
					                        password, sender=user_source_connection.source, request=request
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    user_source_connection.user.save()
 | 
					                    user_source_connection.user.save()
 | 
				
			||||||
                return user, user_source_connection.source
 | 
					                return user_source_connection.user, user_source_connection.source
 | 
				
			||||||
            # Password doesn't match, onto next source
 | 
					            # Password doesn't match, onto next source
 | 
				
			||||||
            LOGGER.debug(
 | 
					            LOGGER.debug(
 | 
				
			||||||
                "failed to kinit, password invalid",
 | 
					                "failed to kinit, password invalid",
 | 
				
			||||||
 | 
				
			|||||||
@ -20,13 +20,15 @@ class LDAPBackend(InbuiltBackend):
 | 
				
			|||||||
            return None
 | 
					            return None
 | 
				
			||||||
        for source in LDAPSource.objects.filter(enabled=True):
 | 
					        for source in LDAPSource.objects.filter(enabled=True):
 | 
				
			||||||
            LOGGER.debug("LDAP Auth attempt", source=source)
 | 
					            LOGGER.debug("LDAP Auth attempt", source=source)
 | 
				
			||||||
            user = self.auth_user(source, **kwargs)
 | 
					            user = self.auth_user(request, source, **kwargs)
 | 
				
			||||||
            if user:
 | 
					            if user:
 | 
				
			||||||
                self.set_method("ldap", request, source=source)
 | 
					                self.set_method("ldap", request, source=source)
 | 
				
			||||||
                return user
 | 
					                return user
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def auth_user(self, source: LDAPSource, password: str, **filters: str) -> User | None:
 | 
					    def auth_user(
 | 
				
			||||||
 | 
					        self, request: HttpRequest, source: LDAPSource, password: str, **filters: str
 | 
				
			||||||
 | 
					    ) -> User | None:
 | 
				
			||||||
        """Try to bind as either user_dn or mail with password.
 | 
					        """Try to bind as either user_dn or mail with password.
 | 
				
			||||||
        Returns True on success, otherwise False"""
 | 
					        Returns True on success, otherwise False"""
 | 
				
			||||||
        users = User.objects.filter(**filters)
 | 
					        users = User.objects.filter(**filters)
 | 
				
			||||||
@ -43,7 +45,7 @@ class LDAPBackend(InbuiltBackend):
 | 
				
			|||||||
            if source.password_login_update_internal_password:
 | 
					            if source.password_login_update_internal_password:
 | 
				
			||||||
                # Password given successfully binds to LDAP, so we save it in our Database
 | 
					                # Password given successfully binds to LDAP, so we save it in our Database
 | 
				
			||||||
                LOGGER.debug("Updating user's password in DB", user=user)
 | 
					                LOGGER.debug("Updating user's password in DB", user=user)
 | 
				
			||||||
                user.set_password(password, sender=source)
 | 
					                user.set_password(password, sender=source, request=request)
 | 
				
			||||||
                user.save()
 | 
					                user.save()
 | 
				
			||||||
            return user
 | 
					            return user
 | 
				
			||||||
        # Password doesn't match
 | 
					        # Password doesn't match
 | 
				
			||||||
 | 
				
			|||||||
@ -88,6 +88,55 @@ class TestSCIMUsers(APITestCase):
 | 
				
			|||||||
            ).exists()
 | 
					            ).exists()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_user_create_duplicate_by_username(self):
 | 
				
			||||||
 | 
					        """Test user create"""
 | 
				
			||||||
 | 
					        user = create_test_user()
 | 
				
			||||||
 | 
					        username = generate_id()
 | 
				
			||||||
 | 
					        obj1 = {
 | 
				
			||||||
 | 
					            "userName": username,
 | 
				
			||||||
 | 
					            "externalId": generate_id(),
 | 
				
			||||||
 | 
					            "emails": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "primary": True,
 | 
				
			||||||
 | 
					                    "value": user.email,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        obj2 = obj1.copy()
 | 
				
			||||||
 | 
					        obj2.update({"externalId": generate_id()})
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse(
 | 
				
			||||||
 | 
					                "authentik_sources_scim:v2-users",
 | 
				
			||||||
 | 
					                kwargs={
 | 
				
			||||||
 | 
					                    "source_slug": self.source.slug,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            data=dumps(obj1),
 | 
				
			||||||
 | 
					            content_type=SCIM_CONTENT_TYPE,
 | 
				
			||||||
 | 
					            HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 201)
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            SCIMSourceUser.objects.filter(source=self.source, user__username=username).exists()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
 | 
				
			||||||
 | 
					            ).exists()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse(
 | 
				
			||||||
 | 
					                "authentik_sources_scim:v2-users",
 | 
				
			||||||
 | 
					                kwargs={
 | 
				
			||||||
 | 
					                    "source_slug": self.source.slug,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            data=dumps(obj2),
 | 
				
			||||||
 | 
					            content_type=SCIM_CONTENT_TYPE,
 | 
				
			||||||
 | 
					            HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 409)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_user_property_mappings(self):
 | 
					    def test_user_property_mappings(self):
 | 
				
			||||||
        """Test user property_mappings"""
 | 
					        """Test user property_mappings"""
 | 
				
			||||||
        self.source.user_property_mappings.set(
 | 
					        self.source.user_property_mappings.set(
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.db.transaction import atomic
 | 
					from django.db.transaction import atomic
 | 
				
			||||||
from django.http import Http404, QueryDict
 | 
					from django.http import Http404, QueryDict
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
@ -113,8 +114,11 @@ class UsersView(SCIMObjectView):
 | 
				
			|||||||
    def post(self, request: Request, **kwargs) -> Response:
 | 
					    def post(self, request: Request, **kwargs) -> Response:
 | 
				
			||||||
        """Create user handler"""
 | 
					        """Create user handler"""
 | 
				
			||||||
        connection = SCIMSourceUser.objects.filter(
 | 
					        connection = SCIMSourceUser.objects.filter(
 | 
				
			||||||
 | 
					            Q(
 | 
				
			||||||
 | 
					                Q(user__uuid=request.data.get("id"))
 | 
				
			||||||
 | 
					                | Q(user__username=request.data.get("userName"))
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
            source=self.source,
 | 
					            source=self.source,
 | 
				
			||||||
            user__uuid=request.data.get("id"),
 | 
					 | 
				
			||||||
        ).first()
 | 
					        ).first()
 | 
				
			||||||
        if connection:
 | 
					        if connection:
 | 
				
			||||||
            self.logger.debug("Found existing user")
 | 
					            self.logger.debug("Found existing user")
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,18 @@
 | 
				
			|||||||
"""AuthenticatorDuoStage API Views"""
 | 
					"""AuthenticatorDuoStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import Http404
 | 
					from django.http import Http404
 | 
				
			||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
 | 
					 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
 | 
				
			||||||
from guardian.shortcuts import get_objects_for_user
 | 
					from guardian.shortcuts import get_objects_for_user
 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.fields import CharField, ChoiceField, IntegerField
 | 
					from rest_framework.fields import CharField, ChoiceField, IntegerField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					from authentik.core.api.groups import GroupMemberSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.flows.api.stages import StageSerializer
 | 
					from authentik.flows.api.stages import StageSerializer
 | 
				
			||||||
@ -168,9 +166,11 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
class DuoDeviceSerializer(ModelSerializer):
 | 
					class DuoDeviceSerializer(ModelSerializer):
 | 
				
			||||||
    """Serializer for Duo authenticator devices"""
 | 
					    """Serializer for Duo authenticator devices"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user = GroupMemberSerializer(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = DuoDevice
 | 
					        model = DuoDevice
 | 
				
			||||||
        fields = ["pk", "name"]
 | 
					        fields = ["pk", "name", "user"]
 | 
				
			||||||
        depth = 2
 | 
					        depth = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -189,8 +189,7 @@ class DuoDeviceViewSet(
 | 
				
			|||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
    filterset_fields = ["name"]
 | 
					    filterset_fields = ["name"]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DuoAdminDeviceViewSet(ModelViewSet):
 | 
					class DuoAdminDeviceViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
"""AuthenticatorSMSStage API Views"""
 | 
					"""AuthenticatorSMSStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					from authentik.core.api.groups import GroupMemberSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.flows.api.stages import StageSerializer
 | 
					from authentik.flows.api.stages import StageSerializer
 | 
				
			||||||
@ -44,9 +42,11 @@ class AuthenticatorSMSStageViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
class SMSDeviceSerializer(ModelSerializer):
 | 
					class SMSDeviceSerializer(ModelSerializer):
 | 
				
			||||||
    """Serializer for sms authenticator devices"""
 | 
					    """Serializer for sms authenticator devices"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user = GroupMemberSerializer(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = SMSDevice
 | 
					        model = SMSDevice
 | 
				
			||||||
        fields = ["name", "pk", "phone_number"]
 | 
					        fields = ["name", "pk", "phone_number", "user"]
 | 
				
			||||||
        depth = 2
 | 
					        depth = 2
 | 
				
			||||||
        extra_kwargs = {
 | 
					        extra_kwargs = {
 | 
				
			||||||
            "phone_number": {"read_only": True},
 | 
					            "phone_number": {"read_only": True},
 | 
				
			||||||
@ -65,11 +65,10 @@ class SMSDeviceViewSet(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = SMSDevice.objects.all()
 | 
					    queryset = SMSDevice.objects.all()
 | 
				
			||||||
    serializer_class = SMSDeviceSerializer
 | 
					    serializer_class = SMSDeviceSerializer
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
    filterset_fields = ["name"]
 | 
					    filterset_fields = ["name"]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					    owner_field = "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SMSAdminDeviceViewSet(ModelViewSet):
 | 
					class SMSAdminDeviceViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
"""AuthenticatorStaticStage API Views"""
 | 
					"""AuthenticatorStaticStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					from authentik.core.api.groups import GroupMemberSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.flows.api.stages import StageSerializer
 | 
					from authentik.flows.api.stages import StageSerializer
 | 
				
			||||||
@ -51,10 +49,11 @@ class StaticDeviceSerializer(ModelSerializer):
 | 
				
			|||||||
    """Serializer for static authenticator devices"""
 | 
					    """Serializer for static authenticator devices"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    token_set = StaticDeviceTokenSerializer(many=True, read_only=True)
 | 
					    token_set = StaticDeviceTokenSerializer(many=True, read_only=True)
 | 
				
			||||||
 | 
					    user = GroupMemberSerializer(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = StaticDevice
 | 
					        model = StaticDevice
 | 
				
			||||||
        fields = ["name", "token_set", "pk"]
 | 
					        fields = ["name", "token_set", "pk", "user"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StaticDeviceViewSet(
 | 
					class StaticDeviceViewSet(
 | 
				
			||||||
@ -69,11 +68,10 @@ class StaticDeviceViewSet(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = StaticDevice.objects.filter(confirmed=True)
 | 
					    queryset = StaticDevice.objects.filter(confirmed=True)
 | 
				
			||||||
    serializer_class = StaticDeviceSerializer
 | 
					    serializer_class = StaticDeviceSerializer
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
    filterset_fields = ["name"]
 | 
					    filterset_fields = ["name"]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					    owner_field = "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StaticAdminDeviceViewSet(ModelViewSet):
 | 
					class StaticAdminDeviceViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,10 @@
 | 
				
			|||||||
"""AuthenticatorTOTPStage API Views"""
 | 
					"""AuthenticatorTOTPStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.fields import ChoiceField
 | 
					from rest_framework.fields import ChoiceField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					from authentik.core.api.groups import GroupMemberSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.flows.api.stages import StageSerializer
 | 
					from authentik.flows.api.stages import StageSerializer
 | 
				
			||||||
@ -40,11 +38,14 @@ class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
class TOTPDeviceSerializer(ModelSerializer):
 | 
					class TOTPDeviceSerializer(ModelSerializer):
 | 
				
			||||||
    """Serializer for totp authenticator devices"""
 | 
					    """Serializer for totp authenticator devices"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user = GroupMemberSerializer(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = TOTPDevice
 | 
					        model = TOTPDevice
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            "name",
 | 
					            "name",
 | 
				
			||||||
            "pk",
 | 
					            "pk",
 | 
				
			||||||
 | 
					            "user",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        depth = 2
 | 
					        depth = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,11 +62,10 @@ class TOTPDeviceViewSet(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = TOTPDevice.objects.filter(confirmed=True)
 | 
					    queryset = TOTPDevice.objects.filter(confirmed=True)
 | 
				
			||||||
    serializer_class = TOTPDeviceSerializer
 | 
					    serializer_class = TOTPDeviceSerializer
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
    filterset_fields = ["name"]
 | 
					    filterset_fields = ["name"]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					    owner_field = "user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TOTPAdminDeviceViewSet(ModelViewSet):
 | 
					class TOTPAdminDeviceViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
"""AuthenticatorWebAuthnStage API Views"""
 | 
					"""AuthenticatorWebAuthnStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework.backends import DjangoFilterBackend
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
 | 
					from authentik.core.api.groups import GroupMemberSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
 | 
					from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
 | 
				
			||||||
@ -16,10 +14,11 @@ class WebAuthnDeviceSerializer(ModelSerializer):
 | 
				
			|||||||
    """Serializer for WebAuthn authenticator devices"""
 | 
					    """Serializer for WebAuthn authenticator devices"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    device_type = WebAuthnDeviceTypeSerializer(read_only=True, allow_null=True)
 | 
					    device_type = WebAuthnDeviceTypeSerializer(read_only=True, allow_null=True)
 | 
				
			||||||
 | 
					    user = GroupMemberSerializer(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = WebAuthnDevice
 | 
					        model = WebAuthnDevice
 | 
				
			||||||
        fields = ["pk", "name", "created_on", "device_type", "aaguid"]
 | 
					        fields = ["pk", "name", "created_on", "device_type", "aaguid", "user"]
 | 
				
			||||||
        extra_kwargs = {
 | 
					        extra_kwargs = {
 | 
				
			||||||
            "aaguid": {"read_only": True},
 | 
					            "aaguid": {"read_only": True},
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -40,8 +39,7 @@ class WebAuthnDeviceViewSet(
 | 
				
			|||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
    filterset_fields = ["name"]
 | 
					    filterset_fields = ["name"]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
    permission_classes = [OwnerPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WebAuthnAdminDeviceViewSet(ModelViewSet):
 | 
					class WebAuthnAdminDeviceViewSet(ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,12 +1,8 @@
 | 
				
			|||||||
"""ConsentStage API Views"""
 | 
					"""ConsentStage API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
					 | 
				
			||||||
from guardian.utils import get_anonymous_user
 | 
					 | 
				
			||||||
from rest_framework import mixins
 | 
					from rest_framework import mixins
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					 | 
				
			||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
					from rest_framework.viewsets import GenericViewSet, ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
 | 
					 | 
				
			||||||
from authentik.core.api.applications import ApplicationSerializer
 | 
					from authentik.core.api.applications import ApplicationSerializer
 | 
				
			||||||
from authentik.core.api.used_by import UsedByMixin
 | 
					from authentik.core.api.used_by import UsedByMixin
 | 
				
			||||||
from authentik.core.api.users import UserSerializer
 | 
					from authentik.core.api.users import UserSerializer
 | 
				
			||||||
@ -57,11 +53,4 @@ class UserConsentViewSet(
 | 
				
			|||||||
    filterset_fields = ["user", "application"]
 | 
					    filterset_fields = ["user", "application"]
 | 
				
			||||||
    ordering = ["application", "expires"]
 | 
					    ordering = ["application", "expires"]
 | 
				
			||||||
    search_fields = ["user__username"]
 | 
					    search_fields = ["user__username"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					    owner_field = "user"
 | 
				
			||||||
    filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_queryset(self):
 | 
					 | 
				
			||||||
        user = self.request.user if self.request else get_anonymous_user()
 | 
					 | 
				
			||||||
        if user.is_superuser:
 | 
					 | 
				
			||||||
            return super().get_queryset()
 | 
					 | 
				
			||||||
        return super().get_queryset().filter(user=user.pk)
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
 | 
				
			||||||
 | 
					        ("authentik_stages_consent", "0006_alter_userconsent_expires"),
 | 
				
			||||||
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="userconsent",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_s_expires_0e99e8_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="userconsent",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_s_expirin_8f51e5_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="userconsent",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_s_expirin_e50090_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -71,3 +71,4 @@ class UserConsent(SerializerModel, ExpiringModel):
 | 
				
			|||||||
        unique_together = (("user", "application", "permissions"),)
 | 
					        unique_together = (("user", "application", "permissions"),)
 | 
				
			||||||
        verbose_name = _("User Consent")
 | 
					        verbose_name = _("User Consent")
 | 
				
			||||||
        verbose_name_plural = _("User Consents")
 | 
					        verbose_name_plural = _("User Consents")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.10 on 2025-01-13 18:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0027_auto_20231028_1424"),
 | 
				
			||||||
 | 
					        ("authentik_stages_invitation", "0008_alter_invitation_expires"),
 | 
				
			||||||
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="invitation",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expires"], name="authentik_s_expires_96f4b8_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="invitation",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["expiring"], name="authentik_s_expirin_4f8f35_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="invitation",
 | 
				
			||||||
 | 
					            index=models.Index(
 | 
				
			||||||
 | 
					                fields=["expiring", "expires"], name="authentik_s_expirin_4f8096_idx"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -84,3 +84,4 @@ class Invitation(SerializerModel, ExpiringModel):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Invitation")
 | 
					        verbose_name = _("Invitation")
 | 
				
			||||||
        verbose_name_plural = _("Invitations")
 | 
					        verbose_name_plural = _("Invitations")
 | 
				
			||||||
 | 
					        indexes = ExpiringModel.Meta.indexes
 | 
				
			||||||
 | 
				
			|||||||
@ -104,7 +104,9 @@ class UserWriteStageView(StageView):
 | 
				
			|||||||
        for key, value in data.items():
 | 
					        for key, value in data.items():
 | 
				
			||||||
            setter_name = f"set_{key}"
 | 
					            setter_name = f"set_{key}"
 | 
				
			||||||
            # Check if user has a setter for this key, like set_password
 | 
					            # Check if user has a setter for this key, like set_password
 | 
				
			||||||
            if hasattr(user, setter_name):
 | 
					            if key == "password":
 | 
				
			||||||
 | 
					                user.set_password(value, request=self.request)
 | 
				
			||||||
 | 
					            elif hasattr(user, setter_name):
 | 
				
			||||||
                setter = getattr(user, setter_name)
 | 
					                setter = getattr(user, setter_name)
 | 
				
			||||||
                if callable(setter):
 | 
					                if callable(setter):
 | 
				
			||||||
                    setter(value)
 | 
					                    setter(value)
 | 
				
			||||||
 | 
				
			|||||||
@ -8,11 +8,11 @@ from django.http import HttpResponseNotFound
 | 
				
			|||||||
from django.http.request import urljoin
 | 
					from django.http.request import urljoin
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
				
			||||||
from rest_framework import permissions
 | 
					 | 
				
			||||||
from rest_framework.authentication import get_authorization_header
 | 
					from rest_framework.authentication import get_authorization_header
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.fields import CharField, IntegerField
 | 
					from rest_framework.fields import CharField, IntegerField
 | 
				
			||||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
					from rest_framework.filters import OrderingFilter, SearchFilter
 | 
				
			||||||
 | 
					from rest_framework.permissions import BasePermission
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.serializers import DateTimeField, ModelSerializer
 | 
					from rest_framework.serializers import DateTimeField, ModelSerializer
 | 
				
			||||||
@ -27,7 +27,7 @@ from authentik.recovery.lib import create_admin_group, create_recovery_token
 | 
				
			|||||||
from authentik.tenants.models import Tenant
 | 
					from authentik.tenants.models import Tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TenantApiKeyPermission(permissions.BasePermission):
 | 
					class TenantApiKeyPermission(BasePermission):
 | 
				
			||||||
    """Authentication based on tenants.api_key"""
 | 
					    """Authentication based on tenants.api_key"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def has_permission(self, request: Request, view: View) -> bool:
 | 
					    def has_permission(self, request: Request, view: View) -> bool:
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
    "$schema": "http://json-schema.org/draft-07/schema",
 | 
					    "$schema": "http://json-schema.org/draft-07/schema",
 | 
				
			||||||
    "$id": "https://goauthentik.io/blueprints/schema.json",
 | 
					    "$id": "https://goauthentik.io/blueprints/schema.json",
 | 
				
			||||||
    "type": "object",
 | 
					    "type": "object",
 | 
				
			||||||
    "title": "authentik 2024.12.1 Blueprint schema",
 | 
					    "title": "authentik 2024.12.2 Blueprint schema",
 | 
				
			||||||
    "required": [
 | 
					    "required": [
 | 
				
			||||||
        "version",
 | 
					        "version",
 | 
				
			||||||
        "entries"
 | 
					        "entries"
 | 
				
			||||||
@ -3884,8 +3884,7 @@
 | 
				
			|||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        "type": "object",
 | 
					                        "type": "object",
 | 
				
			||||||
                        "required": [
 | 
					                        "required": [
 | 
				
			||||||
                            "model",
 | 
					                            "model"
 | 
				
			||||||
                            "identifiers"
 | 
					 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                        "properties": {
 | 
					                        "properties": {
 | 
				
			||||||
                            "model": {
 | 
					                            "model": {
 | 
				
			||||||
@ -3915,9 +3914,6 @@
 | 
				
			|||||||
                            },
 | 
					                            },
 | 
				
			||||||
                            "attrs": {
 | 
					                            "attrs": {
 | 
				
			||||||
                                "$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
 | 
					                                "$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            "identifiers": {
 | 
					 | 
				
			||||||
                                "$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
 | 
					 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
@ -4163,7 +4159,7 @@
 | 
				
			|||||||
                "re_evaluate_policies": {
 | 
					                "re_evaluate_policies": {
 | 
				
			||||||
                    "type": "boolean",
 | 
					                    "type": "boolean",
 | 
				
			||||||
                    "title": "Re evaluate policies",
 | 
					                    "title": "Re evaluate policies",
 | 
				
			||||||
                    "description": "Evaluate policies when the Stage is present to the user."
 | 
					                    "description": "Evaluate policies when the Stage is presented to the user."
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "order": {
 | 
					                "order": {
 | 
				
			||||||
                    "type": "integer",
 | 
					                    "type": "integer",
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ services:
 | 
				
			|||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - redis:/data
 | 
					      - redis:/data
 | 
				
			||||||
  server:
 | 
					  server:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.1}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.2}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: server
 | 
					    command: server
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
@ -54,7 +54,7 @@ services:
 | 
				
			|||||||
      redis:
 | 
					      redis:
 | 
				
			||||||
        condition: service_healthy
 | 
					        condition: service_healthy
 | 
				
			||||||
  worker:
 | 
					  worker:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.1}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.2}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: worker
 | 
					    command: worker
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								go.mod
									
									
									
									
									
								
							@ -6,10 +6,10 @@ toolchain go1.23.0
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	beryju.io/ldap v0.1.0
 | 
						beryju.io/ldap v0.1.0
 | 
				
			||||||
	github.com/coreos/go-oidc/v3 v3.11.0
 | 
						github.com/coreos/go-oidc/v3 v3.12.0
 | 
				
			||||||
	github.com/getsentry/sentry-go v0.30.0
 | 
						github.com/getsentry/sentry-go v0.31.1
 | 
				
			||||||
	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
 | 
						github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
 | 
				
			||||||
	github.com/go-ldap/ldap/v3 v3.4.9
 | 
						github.com/go-ldap/ldap/v3 v3.4.10
 | 
				
			||||||
	github.com/go-openapi/runtime v0.28.0
 | 
						github.com/go-openapi/runtime v0.28.0
 | 
				
			||||||
	github.com/golang-jwt/jwt/v5 v5.2.1
 | 
						github.com/golang-jwt/jwt/v5 v5.2.1
 | 
				
			||||||
	github.com/google/uuid v1.6.0
 | 
						github.com/google/uuid v1.6.0
 | 
				
			||||||
@ -29,9 +29,9 @@ require (
 | 
				
			|||||||
	github.com/spf13/cobra v1.8.1
 | 
						github.com/spf13/cobra v1.8.1
 | 
				
			||||||
	github.com/stretchr/testify v1.10.0
 | 
						github.com/stretchr/testify v1.10.0
 | 
				
			||||||
	github.com/wwt/guac v1.3.2
 | 
						github.com/wwt/guac v1.3.2
 | 
				
			||||||
	goauthentik.io/api/v3 v3.2024105.3
 | 
						goauthentik.io/api/v3 v3.2024122.2
 | 
				
			||||||
	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
 | 
						golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
 | 
				
			||||||
	golang.org/x/oauth2 v0.24.0
 | 
						golang.org/x/oauth2 v0.25.0
 | 
				
			||||||
	golang.org/x/sync v0.10.0
 | 
						golang.org/x/sync v0.10.0
 | 
				
			||||||
	gopkg.in/yaml.v2 v2.4.0
 | 
						gopkg.in/yaml.v2 v2.4.0
 | 
				
			||||||
	layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
 | 
						layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										25
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								go.sum
									
									
									
									
									
								
							@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 | 
				
			|||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 | 
					github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 | 
				
			||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
					github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
				
			||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 | 
					github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 | 
				
			||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
 | 
					github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
 | 
				
			||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 | 
					github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 | 
				
			||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 | 
					github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
					github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
					github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
				
			||||||
@ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
 | 
				
			|||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
					github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
				
			||||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 | 
					github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 | 
				
			||||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 | 
					github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 | 
				
			||||||
github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=
 | 
					github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4=
 | 
				
			||||||
github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=
 | 
					github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
 | 
				
			||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
 | 
					github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
 | 
				
			||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 | 
					github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 | 
				
			||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 | 
					github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 | 
				
			||||||
@ -86,8 +86,8 @@ github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9
 | 
				
			|||||||
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
 | 
					github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
 | 
				
			||||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
 | 
					github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
 | 
				
			||||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
 | 
					github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
 | 
				
			||||||
github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo=
 | 
					github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
 | 
				
			||||||
github.com/go-ldap/ldap/v3 v3.4.9/go.mod h1:+CE/4PPOOdEPGTi2B7qXKQOq+pNBvXZtlBNcVZY0AWI=
 | 
					github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
 | 
				
			||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 | 
					github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 | 
				
			||||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
 | 
					github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
 | 
				
			||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 | 
					github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 | 
				
			||||||
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
 | 
				
			|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 | 
					go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 | 
				
			||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 | 
					go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 | 
				
			||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 | 
					go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 | 
				
			||||||
goauthentik.io/api/v3 v3.2024105.3 h1:Vl1vwPkCtA8hChsxwO3NUI8nupFC7r93jUHvqM+kYVw=
 | 
					goauthentik.io/api/v3 v3.2024122.2 h1:QC+ZQ+AxlPwl9OG1X/Z62EVepmTGyfvJUxhUdFjs+4s=
 | 
				
			||||||
goauthentik.io/api/v3 v3.2024105.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
 | 
					goauthentik.io/api/v3 v3.2024122.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
					golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
					golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
@ -312,7 +312,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
 | 
				
			|||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 | 
					golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 | 
				
			||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 | 
					golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 | 
				
			||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 | 
					golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 | 
				
			||||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 | 
					 | 
				
			||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
 | 
					golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
 | 
				
			||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 | 
					golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 | 
				
			||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
					golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
				
			||||||
@ -386,16 +385,16 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 | 
				
			|||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 | 
					golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 | 
				
			||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 | 
					golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 | 
				
			||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 | 
					golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 | 
				
			||||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
 | 
					golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 | 
				
			||||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
 | 
					golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 | 
				
			||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
					golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
				
			||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
					golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
					golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
					golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
					golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
				
			||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 | 
					golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 | 
				
			||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
 | 
					golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
 | 
				
			||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 | 
					golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 | 
				
			||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
					golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
					golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
					golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
				
			|||||||
@ -26,14 +26,14 @@ type Config struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RedisConfig struct {
 | 
					type RedisConfig struct {
 | 
				
			||||||
	Host      string  `yaml:"host" env:"HOST, overwrite"`
 | 
						Host      string `yaml:"host" env:"HOST, overwrite"`
 | 
				
			||||||
	Port      int     `yaml:"port" env:"PORT, overwrite"`
 | 
						Port      int    `yaml:"port" env:"PORT, overwrite"`
 | 
				
			||||||
	DB        int     `yaml:"db" env:"DB, overwrite"`
 | 
						DB        int    `yaml:"db" env:"DB, overwrite"`
 | 
				
			||||||
	Username  string  `yaml:"username" env:"USERNAME, overwrite"`
 | 
						Username  string `yaml:"username" env:"USERNAME, overwrite"`
 | 
				
			||||||
	Password  string  `yaml:"password" env:"PASSWORD, overwrite"`
 | 
						Password  string `yaml:"password" env:"PASSWORD, overwrite"`
 | 
				
			||||||
	TLS       bool    `yaml:"tls" env:"TLS, overwrite"`
 | 
						TLS       bool   `yaml:"tls" env:"TLS, overwrite"`
 | 
				
			||||||
	TLSReqs   string  `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
 | 
						TLSReqs   string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
 | 
				
			||||||
	TLSCaCert *string `yaml:"tls_ca_certs" env:"TLS_CA_CERT, overwrite"`
 | 
						TLSCaCert string `yaml:"tls_ca_certs" env:"TLS_CA_CERT, overwrite"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ListenConfig struct {
 | 
					type ListenConfig struct {
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,7 @@ func BUILD(def string) string {
 | 
				
			|||||||
func FullVersion() string {
 | 
					func FullVersion() string {
 | 
				
			||||||
	ver := VERSION
 | 
						ver := VERSION
 | 
				
			||||||
	if b := BUILD(""); b != "" {
 | 
						if b := BUILD(""); b != "" {
 | 
				
			||||||
		ver = fmt.Sprintf("%s.%s", ver, b)
 | 
							return fmt.Sprintf("%s+%s", ver, b)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return ver
 | 
						return ver
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -29,4 +29,4 @@ func UserAgent() string {
 | 
				
			|||||||
	return fmt.Sprintf("authentik@%s", FullVersion())
 | 
						return fmt.Sprintf("authentik@%s", FullVersion())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VERSION = "2024.12.1"
 | 
					const VERSION = "2024.12.2"
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@ -16,13 +17,16 @@ import (
 | 
				
			|||||||
	"goauthentik.io/internal/constants"
 | 
						"goauthentik.io/internal/constants"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ac *APIController) getWebsocketURL(akURL url.URL, outpostUUID string) *url.URL {
 | 
					func (ac *APIController) getWebsocketURL(akURL url.URL, outpostUUID string, query url.Values) *url.URL {
 | 
				
			||||||
	wsUrl := &url.URL{}
 | 
						wsUrl := &url.URL{}
 | 
				
			||||||
	wsUrl.Scheme = strings.ReplaceAll(akURL.Scheme, "http", "ws")
 | 
						wsUrl.Scheme = strings.ReplaceAll(akURL.Scheme, "http", "ws")
 | 
				
			||||||
	wsUrl.Host = akURL.Host
 | 
						wsUrl.Host = akURL.Host
 | 
				
			||||||
	_p, _ := url.JoinPath(akURL.Path, "ws/outpost/", outpostUUID)
 | 
						_p, _ := url.JoinPath(akURL.Path, "ws/outpost/", outpostUUID, "/")
 | 
				
			||||||
	wsUrl.Path = _p
 | 
						wsUrl.Path = _p
 | 
				
			||||||
	wsUrl.RawQuery = akURL.Query().Encode()
 | 
						v := url.Values{}
 | 
				
			||||||
 | 
						maps.Insert(v, maps.All(akURL.Query()))
 | 
				
			||||||
 | 
						maps.Insert(v, maps.All(query))
 | 
				
			||||||
 | 
						wsUrl.RawQuery = v.Encode()
 | 
				
			||||||
	return wsUrl
 | 
						return wsUrl
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -45,7 +49,9 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
 | 
				
			|||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ws, _, err := dialer.Dial(ac.getWebsocketURL(akURL, outpostUUID).String(), header)
 | 
						wsu := ac.getWebsocketURL(akURL, outpostUUID, query).String()
 | 
				
			||||||
 | 
						ac.logger.WithField("url", wsu).Debug("connecting to websocket")
 | 
				
			||||||
 | 
						ws, _, err := dialer.Dial(wsu, header)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ac.logger.WithError(err).Warning("failed to connect websocket")
 | 
							ac.logger.WithError(err).Warning("failed to connect websocket")
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
 | 
				
			|||||||
@ -19,14 +19,24 @@ func TestWebsocketURL(t *testing.T) {
 | 
				
			|||||||
	u := URLMustParse("http://localhost:9000?foo=bar")
 | 
						u := URLMustParse("http://localhost:9000?foo=bar")
 | 
				
			||||||
	uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
 | 
						uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
 | 
				
			||||||
	ac := &APIController{}
 | 
						ac := &APIController{}
 | 
				
			||||||
	nu := ac.getWebsocketURL(*u, uuid)
 | 
						nu := ac.getWebsocketURL(*u, uuid, url.Values{})
 | 
				
			||||||
	assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77?foo=bar", nu.String())
 | 
						assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?foo=bar", nu.String())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestWebsocketURL_Query(t *testing.T) {
 | 
				
			||||||
 | 
						u := URLMustParse("http://localhost:9000?foo=bar")
 | 
				
			||||||
 | 
						uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
 | 
				
			||||||
 | 
						ac := &APIController{}
 | 
				
			||||||
 | 
						v := url.Values{}
 | 
				
			||||||
 | 
						v.Set("bar", "baz")
 | 
				
			||||||
 | 
						nu := ac.getWebsocketURL(*u, uuid, v)
 | 
				
			||||||
 | 
						assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?bar=baz&foo=bar", nu.String())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestWebsocketURL_Subpath(t *testing.T) {
 | 
					func TestWebsocketURL_Subpath(t *testing.T) {
 | 
				
			||||||
	u := URLMustParse("http://localhost:9000/foo/bar/")
 | 
						u := URLMustParse("http://localhost:9000/foo/bar/")
 | 
				
			||||||
	uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
 | 
						uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
 | 
				
			||||||
	ac := &APIController{}
 | 
						ac := &APIController{}
 | 
				
			||||||
	nu := ac.getWebsocketURL(*u, uuid)
 | 
						nu := ac.getWebsocketURL(*u, uuid, url.Values{})
 | 
				
			||||||
	assert.Equal(t, "ws://localhost:9000/foo/bar/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77", nu.String())
 | 
						assert.Equal(t, "ws://localhost:9000/foo/bar/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/", nu.String())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -45,15 +45,15 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
 | 
				
			|||||||
				break
 | 
									break
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			ca := config.Get().Redis.TLSCaCert
 | 
								ca := config.Get().Redis.TLSCaCert
 | 
				
			||||||
			if ca != nil {
 | 
								if ca != "" {
 | 
				
			||||||
				// Get the SystemCertPool, continue with an empty pool on error
 | 
									// Get the SystemCertPool, continue with an empty pool on error
 | 
				
			||||||
				rootCAs, _ := x509.SystemCertPool()
 | 
									rootCAs, _ := x509.SystemCertPool()
 | 
				
			||||||
				if rootCAs == nil {
 | 
									if rootCAs == nil {
 | 
				
			||||||
					rootCAs = x509.NewCertPool()
 | 
										rootCAs = x509.NewCertPool()
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				certs, err := os.ReadFile(*ca)
 | 
									certs, err := os.ReadFile(ca)
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
					a.log.WithError(err).Fatalf("Failed to append %s to RootCAs", *ca)
 | 
										a.log.WithError(err).Fatalf("Failed to append %s to RootCAs", ca)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				// Append our cert to the system pool
 | 
									// Append our cert to the system pool
 | 
				
			||||||
				if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
 | 
									if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,10 @@ import (
 | 
				
			|||||||
	"goauthentik.io/internal/utils/sentry"
 | 
						"goauthentik.io/internal/utils/sentry"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						ErrAuthentikStarting = errors.New("authentik starting")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ws *WebServer) configureProxy() {
 | 
					func (ws *WebServer) configureProxy() {
 | 
				
			||||||
	// Reverse proxy to the application server
 | 
						// Reverse proxy to the application server
 | 
				
			||||||
	director := func(req *http.Request) {
 | 
						director := func(req *http.Request) {
 | 
				
			||||||
@ -38,7 +42,7 @@ func (ws *WebServer) configureProxy() {
 | 
				
			|||||||
	}))
 | 
						}))
 | 
				
			||||||
	ws.mainRouter.PathPrefix(config.Get().Web.Path).HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
 | 
						ws.mainRouter.PathPrefix(config.Get().Web.Path).HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		if !ws.g.IsRunning() {
 | 
							if !ws.g.IsRunning() {
 | 
				
			||||||
			ws.proxyErrorHandler(rw, r, errors.New("authentik starting"))
 | 
								ws.proxyErrorHandler(rw, r, ErrAuthentikStarting)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		before := time.Now()
 | 
							before := time.Now()
 | 
				
			||||||
@ -59,7 +63,9 @@ func (ws *WebServer) configureProxy() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
 | 
					func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
 | 
				
			||||||
	ws.log.WithError(err).Warning("failed to proxy to backend")
 | 
						if !errors.Is(err, ErrAuthentikStarting) {
 | 
				
			||||||
 | 
							ws.log.WithError(err).Warning("failed to proxy to backend")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	rw.WriteHeader(http.StatusBadGateway)
 | 
						rw.WriteHeader(http.StatusBadGateway)
 | 
				
			||||||
	em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
 | 
						em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
 | 
				
			||||||
	// return json if the client asks for json
 | 
						// return json if the client asks for json
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
 | 
					"POT-Creation-Date: 2024-12-20 00:08+0000\n"
 | 
				
			||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
					"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
				
			||||||
@ -101,6 +101,10 @@ msgstr ""
 | 
				
			|||||||
msgid "Brands"
 | 
					msgid "Brands"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/api/application_entitlements.py
 | 
				
			||||||
 | 
					msgid "User does not have access to application."
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: authentik/core/api/devices.py
 | 
					#: authentik/core/api/devices.py
 | 
				
			||||||
msgid "Extra description not available"
 | 
					msgid "Extra description not available"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
@ -225,6 +229,14 @@ msgstr ""
 | 
				
			|||||||
msgid "Applications"
 | 
					msgid "Applications"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/models.py
 | 
				
			||||||
 | 
					msgid "Application Entitlement"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/models.py
 | 
				
			||||||
 | 
					msgid "Application Entitlements"
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: authentik/core/models.py
 | 
					#: authentik/core/models.py
 | 
				
			||||||
msgid "Use the source-specific identifier"
 | 
					msgid "Use the source-specific identifier"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3603
									
								
								locale/fi/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3603
									
								
								locale/fi/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -12,16 +12,16 @@
 | 
				
			|||||||
# Charles Leclerc, 2024
 | 
					# Charles Leclerc, 2024
 | 
				
			||||||
# nerdinator <florian.dupret@gmail.com>, 2024
 | 
					# nerdinator <florian.dupret@gmail.com>, 2024
 | 
				
			||||||
# Tina, 2024
 | 
					# Tina, 2024
 | 
				
			||||||
# Marc Schmitt, 2024
 | 
					# Marc Schmitt, 2025
 | 
				
			||||||
# 
 | 
					# 
 | 
				
			||||||
#, fuzzy
 | 
					#, fuzzy
 | 
				
			||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
 | 
					"POT-Creation-Date: 2024-12-20 00:08+0000\n"
 | 
				
			||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
					"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
				
			||||||
"Last-Translator: Marc Schmitt, 2024\n"
 | 
					"Last-Translator: Marc Schmitt, 2025\n"
 | 
				
			||||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
 | 
					"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
 | 
				
			||||||
"MIME-Version: 1.0\n"
 | 
					"MIME-Version: 1.0\n"
 | 
				
			||||||
"Content-Type: text/plain; charset=UTF-8\n"
 | 
					"Content-Type: text/plain; charset=UTF-8\n"
 | 
				
			||||||
@ -121,6 +121,10 @@ msgstr "Marque"
 | 
				
			|||||||
msgid "Brands"
 | 
					msgid "Brands"
 | 
				
			||||||
msgstr "Marques"
 | 
					msgstr "Marques"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/api/application_entitlements.py
 | 
				
			||||||
 | 
					msgid "User does not have access to application."
 | 
				
			||||||
 | 
					msgstr "L'utilisateur n'a pas accès à l'application."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: authentik/core/api/devices.py
 | 
					#: authentik/core/api/devices.py
 | 
				
			||||||
msgid "Extra description not available"
 | 
					msgid "Extra description not available"
 | 
				
			||||||
msgstr "Description supplémentaire indisponible"
 | 
					msgstr "Description supplémentaire indisponible"
 | 
				
			||||||
@ -256,6 +260,14 @@ msgstr "Application"
 | 
				
			|||||||
msgid "Applications"
 | 
					msgid "Applications"
 | 
				
			||||||
msgstr "Applications"
 | 
					msgstr "Applications"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/models.py
 | 
				
			||||||
 | 
					msgid "Application Entitlement"
 | 
				
			||||||
 | 
					msgstr "Droit applicatif"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/models.py
 | 
				
			||||||
 | 
					msgid "Application Entitlements"
 | 
				
			||||||
 | 
					msgstr "Droits applicatifs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: authentik/core/models.py
 | 
					#: authentik/core/models.py
 | 
				
			||||||
msgid "Use the source-specific identifier"
 | 
					msgid "Use the source-specific identifier"
 | 
				
			||||||
msgstr "Utiliser l'identifiant spécifique à la source"
 | 
					msgstr "Utiliser l'identifiant spécifique à la source"
 | 
				
			||||||
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							@ -15,7 +15,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
 | 
					"POT-Creation-Date: 2024-12-20 00:08+0000\n"
 | 
				
			||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
					"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
				
			||||||
"Last-Translator: deluxghost, 2024\n"
 | 
					"Last-Translator: deluxghost, 2024\n"
 | 
				
			||||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
 | 
					"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
 | 
				
			||||||
@ -110,6 +110,10 @@ msgstr "品牌"
 | 
				
			|||||||
msgid "Brands"
 | 
					msgid "Brands"
 | 
				
			||||||
msgstr "品牌"
 | 
					msgstr "品牌"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/api/application_entitlements.py
 | 
				
			||||||
 | 
					msgid "User does not have access to application."
 | 
				
			||||||
 | 
					msgstr "用户没有访问此应用程序的权限。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: authentik/core/api/devices.py
 | 
					#: authentik/core/api/devices.py
 | 
				
			||||||
msgid "Extra description not available"
 | 
					msgid "Extra description not available"
 | 
				
			||||||
msgstr "额外描述不可用"
 | 
					msgstr "额外描述不可用"
 | 
				
			||||||
@ -235,6 +239,14 @@ msgstr "应用程序"
 | 
				
			|||||||
msgid "Applications"
 | 
					msgid "Applications"
 | 
				
			||||||
msgstr "应用程序"
 | 
					msgstr "应用程序"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/models.py
 | 
				
			||||||
 | 
					msgid "Application Entitlement"
 | 
				
			||||||
 | 
					msgstr "应用程序授权"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#: authentik/core/models.py
 | 
				
			||||||
 | 
					msgid "Application Entitlements"
 | 
				
			||||||
 | 
					msgstr "应用程序授权"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: authentik/core/models.py
 | 
					#: authentik/core/models.py
 | 
				
			||||||
msgid "Use the source-specific identifier"
 | 
					msgid "Use the source-specific identifier"
 | 
				
			||||||
msgstr "使用源特定的标识符"
 | 
					msgstr "使用源特定的标识符"
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user