Compare commits
	
		
			60 Commits
		
	
	
		
			20240219-m
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1c03cfa906 | |||
| e2dbab5bca | |||
| 3a6c42fefb | |||
| 6bb180f94e | |||
| 03dea17519 | |||
| 49d83f11bd | |||
| 5f0af81e4d | |||
| 63591e1710 | |||
| 6503a7b048 | |||
| 7e244e0679 | |||
| c1998bf3f2 | |||
| 83372618a8 | |||
| 89a876e141 | |||
| 26d6e8bc5c | |||
| d9dc373170 | |||
| 4ec37c5239 | |||
| a9cfa6fe35 | |||
| 5ac5084149 | |||
| eda38a30b1 | |||
| 9b84bf7174 | |||
| f74549be6d | |||
| 76f4d7fb0a | |||
| d1cf1dd083 | |||
| 2835fbd390 | |||
| 76ad2c8925 | |||
| 2270629fdc | |||
| 43a629efc1 | |||
| 4044e52403 | |||
| aa7c846467 | |||
| 8ab7f4073b | |||
| a05856c2ef | |||
| 9e9154e04a | |||
| 32549066c0 | |||
| 5ed3e879a2 | |||
| 4e4923ad0e | |||
| 0302d147e9 | |||
| 8256f1897d | |||
| 16d321835d | |||
| f34612efe6 | |||
| e82f147130 | |||
| 0ea6ad8eea | |||
| f731443220 | |||
| b70a66cde5 | |||
| b733dbbcb0 | |||
| e34d4c0669 | |||
| 310983a4d0 | |||
| 47b0fc86f7 | |||
| b6e961b1f3 | |||
| 874d7ff320 | |||
| e4a5bc9df6 | |||
| 318e0cf9f8 | |||
| bd0815d894 | |||
| af35ecfe66 | |||
| 0c05cd64bb | |||
| cb80b76490 | |||
| 061d4bc758 | |||
| 8ff27f69e1 | |||
| 045cd98276 | |||
| b520843984 | |||
| 92216e4ea8 | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2023.10.7 | ||||
| current_version = 2024.2.4 | ||||
| tag = 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*))? | ||||
|  | ||||
| @ -6,6 +6,7 @@ build/** | ||||
| build_docs/** | ||||
| *Dockerfile | ||||
| blueprints/local | ||||
| .git | ||||
| !gen-ts-api/node_modules | ||||
| !gen-ts-api/dist/** | ||||
| !gen-go-api/ | ||||
|  | ||||
							
								
								
									
										69
									
								
								.github/actions/docker-push-variables/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										69
									
								
								.github/actions/docker-push-variables/action.yml
									
									
									
									
										vendored
									
									
								
							| @ -11,6 +11,10 @@ inputs: | ||||
|     description: "Docker image arch" | ||||
|  | ||||
| outputs: | ||||
|   shouldBuild: | ||||
|     description: "Whether to build image or not" | ||||
|     value: ${{ steps.ev.outputs.shouldBuild }} | ||||
|  | ||||
|   sha: | ||||
|     description: "sha" | ||||
|     value: ${{ steps.ev.outputs.sha }} | ||||
| @ -34,63 +38,10 @@ runs: | ||||
|   steps: | ||||
|     - name: Generate config | ||||
|       id: ev | ||||
|       shell: python | ||||
|       shell: bash | ||||
|       env: | ||||
|         IMAGE_NAME: ${{ inputs.image-name }} | ||||
|         IMAGE_ARCH: ${{ inputs.image-arch }} | ||||
|         PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||||
|       run: | | ||||
|         """Helper script to get the actual branch name, docker safe""" | ||||
|         import configparser | ||||
|         import os | ||||
|         from time import time | ||||
|  | ||||
|         parser = configparser.ConfigParser() | ||||
|         parser.read(".bumpversion.cfg") | ||||
|  | ||||
|         branch_name = os.environ["GITHUB_REF"] | ||||
|         if os.environ.get("GITHUB_HEAD_REF", "") != "": | ||||
|             branch_name = os.environ["GITHUB_HEAD_REF"] | ||||
|         safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-") | ||||
|  | ||||
|         image_names = "${{ inputs.image-name }}".split(",") | ||||
|         image_arch = "${{ inputs.image-arch }}" or None | ||||
|  | ||||
|         is_pull_request = bool("${{ github.event.pull_request.head.sha }}") | ||||
|         is_release = "dev" not in image_names[0] | ||||
|  | ||||
|         sha = os.environ["GITHUB_SHA"] if not is_pull_request else "${{ github.event.pull_request.head.sha }}" | ||||
|  | ||||
|         # 2042.1.0 or 2042.1.0-rc1 | ||||
|         version = parser.get("bumpversion", "current_version") | ||||
|         # 2042.1 | ||||
|         version_family = ".".join(version.split("-", 1)[0].split(".")[:-1]) | ||||
|         prerelease = "-" in version | ||||
|  | ||||
|         image_tags = [] | ||||
|         if is_release: | ||||
|             for name in image_names: | ||||
|                 image_tags += [ | ||||
|                     f"{name}:{version}", | ||||
|                 ] | ||||
|             if not prerelease: | ||||
|                 image_tags += [ | ||||
|                     f"{name}:latest", | ||||
|                     f"{name}:{version_family}", | ||||
|                 ] | ||||
|         else: | ||||
|             suffix = "" | ||||
|             if image_arch and image_arch != "amd64": | ||||
|                 suffix = f"-{image_arch}" | ||||
|             for name in image_names: | ||||
|                 image_tags += [ | ||||
|                     f"{name}:gh-{sha}{suffix}",  # Used for ArgoCD and PR comments | ||||
|                     f"{name}:gh-{safe_branch_name}{suffix}",  # For convenience | ||||
|                     f"{name}:gh-{safe_branch_name}-{int(time())}-{sha[:7]}{suffix}",  # Use by FluxCD | ||||
|                 ] | ||||
|  | ||||
|         image_main_tag = image_tags[0] | ||||
|         image_tags_rendered = ",".join(image_tags) | ||||
|  | ||||
|         with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output: | ||||
|             print("sha=%s" % sha, file=_output) | ||||
|             print("version=%s" % version, file=_output) | ||||
|             print("prerelease=%s" % prerelease, file=_output) | ||||
|             print("imageTags=%s" % image_tags_rendered, file=_output) | ||||
|             print("imageMainTag=%s" % image_main_tag, file=_output) | ||||
|         python3 ${{ github.action_path }}/push_vars.py | ||||
|  | ||||
							
								
								
									
										62
									
								
								.github/actions/docker-push-variables/push_vars.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								.github/actions/docker-push-variables/push_vars.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| """Helper script to get the actual branch name, docker safe""" | ||||
|  | ||||
| import configparser | ||||
| import os | ||||
| from time import time | ||||
|  | ||||
| parser = configparser.ConfigParser() | ||||
| parser.read(".bumpversion.cfg") | ||||
|  | ||||
| should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower() | ||||
|  | ||||
| branch_name = os.environ["GITHUB_REF"] | ||||
| if os.environ.get("GITHUB_HEAD_REF", "") != "": | ||||
|     branch_name = os.environ["GITHUB_HEAD_REF"] | ||||
| safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-") | ||||
|  | ||||
| image_names = os.getenv("IMAGE_NAME").split(",") | ||||
| image_arch = os.getenv("IMAGE_ARCH") or None | ||||
|  | ||||
| is_pull_request = bool(os.getenv("PR_HEAD_SHA")) | ||||
| is_release = "dev" not in image_names[0] | ||||
|  | ||||
| sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA") | ||||
|  | ||||
| # 2042.1.0 or 2042.1.0-rc1 | ||||
| version = parser.get("bumpversion", "current_version") | ||||
| # 2042.1 | ||||
| version_family = ".".join(version.split("-", 1)[0].split(".")[:-1]) | ||||
| prerelease = "-" in version | ||||
|  | ||||
| image_tags = [] | ||||
| if is_release: | ||||
|     for name in image_names: | ||||
|         image_tags += [ | ||||
|             f"{name}:{version}", | ||||
|         ] | ||||
|         if not prerelease: | ||||
|             image_tags += [ | ||||
|                 f"{name}:latest", | ||||
|                 f"{name}:{version_family}", | ||||
|             ] | ||||
| else: | ||||
|     suffix = "" | ||||
|     if image_arch and image_arch != "amd64": | ||||
|         suffix = f"-{image_arch}" | ||||
|     for name in image_names: | ||||
|         image_tags += [ | ||||
|             f"{name}:gh-{sha}{suffix}",  # Used for ArgoCD and PR comments | ||||
|             f"{name}:gh-{safe_branch_name}{suffix}",  # For convenience | ||||
|             f"{name}:gh-{safe_branch_name}-{int(time())}-{sha[:7]}{suffix}",  # Use by FluxCD | ||||
|         ] | ||||
|  | ||||
| image_main_tag = image_tags[0] | ||||
| image_tags_rendered = ",".join(image_tags) | ||||
|  | ||||
| with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output: | ||||
|     print("shouldBuild=%s" % should_build, file=_output) | ||||
|     print("sha=%s" % sha, file=_output) | ||||
|     print("version=%s" % version, file=_output) | ||||
|     print("prerelease=%s" % prerelease, file=_output) | ||||
|     print("imageTags=%s" % image_tags_rendered, file=_output) | ||||
|     print("imageMainTag=%s" % image_main_tag, file=_output) | ||||
							
								
								
									
										7
									
								
								.github/actions/docker-push-variables/test.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								.github/actions/docker-push-variables/test.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1,7 @@ | ||||
| #!/bin/bash -x | ||||
| SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) | ||||
| GITHUB_OUTPUT=/dev/stdout \ | ||||
|     GITHUB_REF=ref \ | ||||
|     GITHUB_SHA=sha \ | ||||
|     IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \ | ||||
|     python $SCRIPT_DIR/push_vars.py | ||||
							
								
								
									
										8
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -219,7 +219,6 @@ jobs: | ||||
|       # Needed to upload contianer images to ghcr.io | ||||
|       packages: write | ||||
|     timeout-minutes: 120 | ||||
|     if: "github.repository == 'goauthentik/authentik'" | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
| @ -231,10 +230,13 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/dev-server | ||||
|           image-arch: ${{ matrix.arch }} | ||||
|       - name: Login to Container Registry | ||||
|         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
| @ -250,7 +252,7 @@ jobs: | ||||
|             GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }} | ||||
|             GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }} | ||||
|           tags: ${{ steps.ev.outputs.imageTags }} | ||||
|           push: true | ||||
|           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|           build-args: | | ||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||
|           cache-from: type=gha | ||||
| @ -272,6 +274,8 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/dev-server | ||||
|       - name: Comment on PR | ||||
|  | ||||
							
								
								
									
										6
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -71,7 +71,6 @@ jobs: | ||||
|     permissions: | ||||
|       # Needed to upload contianer images to ghcr.io | ||||
|       packages: write | ||||
|     if: "github.repository == 'goauthentik/authentik'" | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
| @ -83,9 +82,12 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/dev-${{ matrix.type }} | ||||
|       - name: Login to Container Registry | ||||
|         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
| @ -98,7 +100,7 @@ jobs: | ||||
|         with: | ||||
|           tags: ${{ steps.ev.outputs.imageTags }} | ||||
|           file: ${{ matrix.type }}.Dockerfile | ||||
|           push: true | ||||
|           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|           build-args: | | ||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|  | ||||
							
								
								
									
										6
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -20,6 +20,8 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/server,beryju/authentik | ||||
|       - name: Docker Login Registry | ||||
| @ -72,6 +74,8 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }} | ||||
|       - name: make empty clients | ||||
| @ -168,6 +172,8 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/server | ||||
|       - name: Get static files from docker image | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -32,6 +32,8 @@ jobs: | ||||
|       - name: prepare variables | ||||
|         uses: ./.github/actions/docker-push-variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||
|         with: | ||||
|           image-name: ghcr.io/goauthentik/server | ||||
|       - name: Create Release | ||||
|  | ||||
| @ -27,7 +27,6 @@ WORKDIR /work/web | ||||
|  | ||||
| RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \ | ||||
|     --mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \ | ||||
|     --mount=type=bind,target=/work/.git,src=./.git,readonly \ | ||||
|     --mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \ | ||||
|     npm ci --include=dev | ||||
|  | ||||
| @ -104,9 +103,10 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \ | ||||
|     --mount=type=cache,target=/root/.cache/pip \ | ||||
|     --mount=type=cache,target=/root/.cache/pypoetry \ | ||||
|     python -m venv /ak-root/venv/ && \ | ||||
|     bash -c "source ${VENV_PATH}/bin/activate && \ | ||||
|         pip3 install --upgrade pip && \ | ||||
|         pip3 install poetry && \ | ||||
|     poetry install --only=main --no-ansi --no-interaction | ||||
|         poetry install --only=main --no-ansi --no-interaction --no-root" | ||||
|  | ||||
| # Stage 6: Run | ||||
| FROM docker.io/python:3.12.2-slim-bookworm AS final-image | ||||
|  | ||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @ -5,7 +5,7 @@ PWD = $(shell pwd) | ||||
| UID = $(shell id -u) | ||||
| GID = $(shell id -g) | ||||
| NPM_VERSION = $(shell python -m scripts.npm_version) | ||||
| PY_SOURCES = authentik tests scripts lifecycle | ||||
| PY_SOURCES = authentik tests scripts lifecycle .github | ||||
| DOCKER_IMAGE ?= "authentik:test" | ||||
|  | ||||
| GEN_API_TS = "gen-ts-api" | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| from os import environ | ||||
| from typing import Optional | ||||
|  | ||||
| __version__ = "2023.10.7" | ||||
| __version__ = "2024.2.4" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,35 +0,0 @@ | ||||
| """test decorators api""" | ||||
|  | ||||
| from django.urls import reverse | ||||
| from guardian.shortcuts import assign_perm | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import Application, User | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
|  | ||||
| class TestAPIDecorators(APITestCase): | ||||
|     """test decorators api""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.user = User.objects.create(username="test-user") | ||||
|  | ||||
|     def test_obj_perm_denied(self): | ||||
|         """Test object perm denied""" | ||||
|         self.client.force_login(self.user) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 403) | ||||
|  | ||||
|     def test_other_perm_denied(self): | ||||
|         """Test other perm denied""" | ||||
|         self.client.force_login(self.user) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||
|         assign_perm("authentik_core.view_application", self.user, app) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 403) | ||||
| @ -68,7 +68,11 @@ class ConfigView(APIView): | ||||
|         """Get all capabilities this server instance supports""" | ||||
|         caps = [] | ||||
|         deb_test = settings.DEBUG or settings.TEST | ||||
|         if Path(settings.MEDIA_ROOT).is_mount() or deb_test: | ||||
|         if ( | ||||
|             CONFIG.get("storage.media.backend", "file") == "s3" | ||||
|             or Path(settings.STORAGES["default"]["OPTIONS"]["location"]).is_mount() | ||||
|             or deb_test | ||||
|         ): | ||||
|             caps.append(Capabilities.CAN_SAVE_MEDIA) | ||||
|         for processor in get_context_processors(): | ||||
|             if cap := processor.capability(): | ||||
|  | ||||
| @ -10,13 +10,13 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ListSerializer, ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.models import BlueprintInstance | ||||
| from authentik.blueprints.v1.importer import Importer | ||||
| from authentik.blueprints.v1.oci import OCI_PREFIX | ||||
| from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import JSONDictField, PassiveSerializer | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class ManagedSerializer: | ||||
|  | ||||
| @ -74,7 +74,7 @@ class Exporter: | ||||
|  | ||||
|  | ||||
| class FlowExporter(Exporter): | ||||
|     """Exporter customised to only return objects related to `flow`""" | ||||
|     """Exporter customized to only return objects related to `flow`""" | ||||
|  | ||||
|     flow: Flow | ||||
|     with_policies: bool | ||||
|  | ||||
| @ -9,6 +9,7 @@ from sentry_sdk.hub import Hub | ||||
|  | ||||
| from authentik import get_full_version | ||||
| from authentik.brands.models import Brand | ||||
| from authentik.tenants.models import Tenant | ||||
|  | ||||
| _q_default = Q(default=True) | ||||
| DEFAULT_BRAND = Brand(domain="fallback") | ||||
| @ -30,13 +31,14 @@ def get_brand_for_request(request: HttpRequest) -> Brand: | ||||
| def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||
|     """Context Processor that injects brand object into every template""" | ||||
|     brand = getattr(request, "brand", DEFAULT_BRAND) | ||||
|     tenant = getattr(request, "tenant", Tenant()) | ||||
|     trace = "" | ||||
|     span = Hub.current.scope.span | ||||
|     if span: | ||||
|         trace = span.to_traceparent() | ||||
|     return { | ||||
|         "brand": brand, | ||||
|         "footer_links": request.tenant.footer_links, | ||||
|         "footer_links": tenant.footer_links, | ||||
|         "sentry_trace": trace, | ||||
|         "version": get_full_version(), | ||||
|     } | ||||
|  | ||||
| @ -23,7 +23,6 @@ from structlog.stdlib import get_logger | ||||
| from structlog.testing import capture_logs | ||||
|  | ||||
| from authentik.admin.api.metrics import CoordinateSerializer | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| @ -39,6 +38,7 @@ from authentik.lib.utils.file import ( | ||||
| from authentik.policies.api.exec import PolicyTestResultSerializer | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.types import PolicyResult | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.rbac.filters import ObjectFilter | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -15,11 +15,11 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import JSONDictField, PassiveSerializer | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.rbac.api.roles import RoleSerializer | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class GroupMemberSerializer(ModelSerializer): | ||||
|  | ||||
| @ -14,7 +14,6 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.api import ManagedSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer | ||||
| @ -23,6 +22,7 @@ from authentik.core.models import PropertyMapping | ||||
| from authentik.events.utils import sanitize_item | ||||
| from authentik.lib.utils.reflection import all_subclasses | ||||
| from authentik.policies.api.exec import PolicyTestSerializer | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class PropertyMappingTestResultSerializer(PassiveSerializer): | ||||
|  | ||||
| @ -16,7 +16,6 @@ from rest_framework.viewsets import GenericViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer | ||||
| @ -30,6 +29,7 @@ from authentik.lib.utils.file import ( | ||||
| ) | ||||
| from authentik.lib.utils.reflection import all_subclasses | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -15,15 +15,15 @@ from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.authorization import OwnerSuperuserPermissions | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.api import ManagedSerializer | ||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.users import UserSerializer | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents | ||||
| from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.utils import model_to_dict | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class TokenSerializer(ManagedSerializer, ModelSerializer): | ||||
| @ -36,6 +36,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): | ||||
|         if SERIALIZER_CONTEXT_BLUEPRINT in self.context: | ||||
|             self.fields["key"] = CharField(required=False) | ||||
|  | ||||
|     def validate_user(self, user: User): | ||||
|         """Ensure user of token cannot be changed""" | ||||
|         if self.instance and self.instance.user_id: | ||||
|             if user.pk != self.instance.user_id: | ||||
|                 raise ValidationError("User cannot be changed") | ||||
|         return user | ||||
|  | ||||
|     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: | ||||
|         """Ensure only API or App password tokens are created.""" | ||||
|         request: Request = self.context.get("request") | ||||
|  | ||||
| @ -49,7 +49,6 @@ from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.admin.api.metrics import CoordinateSerializer | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||
| from authentik.brands.models import Brand | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| @ -74,6 +73,7 @@ from authentik.flows.models import FlowToken | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | ||||
| from authentik.lib.avatars import get_avatar | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| @ -154,7 +154,7 @@ class UserSerializer(ModelSerializer): | ||||
|  | ||||
|     def get_avatar(self, user: User) -> str: | ||||
|         """User's avatar, either a http/https URL or a data URI""" | ||||
|         return get_avatar(user, self.context["request"]) | ||||
|         return get_avatar(user, self.context.get("request")) | ||||
|  | ||||
|     def validate_path(self, path: str) -> str: | ||||
|         """Validate path""" | ||||
| @ -218,7 +218,7 @@ class UserSelfSerializer(ModelSerializer): | ||||
|  | ||||
|     def get_avatar(self, user: User) -> str: | ||||
|         """User's avatar, either a http/https URL or a data URI""" | ||||
|         return get_avatar(user, self.context["request"]) | ||||
|         return get_avatar(user, self.context.get("request")) | ||||
|  | ||||
|     @extend_schema_field( | ||||
|         ListSerializer( | ||||
| @ -533,7 +533,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|             400: OpenApiResponse(description="Bad request"), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=True, methods=["POST"]) | ||||
|     @action(detail=True, methods=["POST"], permission_classes=[]) | ||||
|     def set_password(self, request: Request, pk: int) -> Response: | ||||
|         """Set password for user""" | ||||
|         user: User = self.get_object() | ||||
| @ -611,7 +611,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         email_stage: EmailStage = stages.first() | ||||
|         message = TemplateEmailMessage( | ||||
|             subject=_(email_stage.subject), | ||||
|             to=[for_user.email], | ||||
|             to=[(for_user.name, for_user.email)], | ||||
|             template_name=email_stage.template, | ||||
|             language=for_user.locale(request), | ||||
|             template_context={ | ||||
| @ -631,7 +631,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|             "401": OpenApiResponse(description="Access denied"), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=True, methods=["POST"]) | ||||
|     @action(detail=True, methods=["POST"], permission_classes=[]) | ||||
|     def impersonate(self, request: Request, pk: int) -> Response: | ||||
|         """Impersonate a user""" | ||||
|         if not request.tenant.impersonation: | ||||
|  | ||||
| @ -7,8 +7,8 @@ from guardian.shortcuts import get_anonymous_user | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.api.tokens import TokenSerializer | ||||
| from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_user | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
|  | ||||
| @ -17,7 +17,7 @@ class TestTokenAPI(APITestCase): | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.user = User.objects.create(username="testuser") | ||||
|         self.user = create_test_user() | ||||
|         self.admin = create_test_admin_user() | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
| @ -76,6 +76,24 @@ class TestTokenAPI(APITestCase): | ||||
|         self.assertEqual(token.intent, TokenIntents.INTENT_API) | ||||
|         self.assertEqual(token.expiring, False) | ||||
|  | ||||
|     def test_token_change_user(self): | ||||
|         """Test creating a token and then changing the user""" | ||||
|         ident = generate_id() | ||||
|         response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident}) | ||||
|         self.assertEqual(response.status_code, 201) | ||||
|         token = Token.objects.get(identifier=ident) | ||||
|         self.assertEqual(token.user, self.user) | ||||
|         self.assertEqual(token.intent, TokenIntents.INTENT_API) | ||||
|         self.assertEqual(token.expiring, True) | ||||
|         self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token)) | ||||
|         response = self.client.put( | ||||
|             reverse("authentik_api:token-detail", kwargs={"identifier": ident}), | ||||
|             data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk}, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         token.refresh_from_db() | ||||
|         self.assertEqual(token.user, self.user) | ||||
|  | ||||
|     def test_list(self): | ||||
|         """Test Token List (Test normal authentication)""" | ||||
|         Token.objects.all().delete() | ||||
|  | ||||
| @ -24,13 +24,13 @@ from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.authorization import SecretKeyFilter | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.crypto.apps import MANAGED_KEY | ||||
| from authentik.crypto.builder import CertificateBuilder | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -44,7 +44,7 @@ class CertificateBuilder: | ||||
|     def generate_private_key(self) -> PrivateKeyTypes: | ||||
|         """Generate private key""" | ||||
|         if self._use_ec_private_key: | ||||
|             return ec.generate_private_key(curve=ec.SECP256R1()) | ||||
|             return ec.generate_private_key(curve=ec.SECP256R1) | ||||
|         return rsa.generate_private_key( | ||||
|             public_exponent=65537, key_size=4096, backend=default_backend() | ||||
|         ) | ||||
|  | ||||
| @ -16,12 +16,12 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.core.models import User, UserTypes | ||||
| from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer | ||||
| from authentik.enterprise.models import License | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.root.install_id import get_install_id | ||||
|  | ||||
|  | ||||
| @ -31,7 +31,7 @@ class EnterpriseRequiredMixin: | ||||
|  | ||||
|     def validate(self, attrs: dict) -> dict: | ||||
|         """Check that a valid license exists""" | ||||
|         if not LicenseKey.cached_summary().valid: | ||||
|         if not LicenseKey.cached_summary().has_license: | ||||
|             raise ValidationError(_("Enterprise is required to create/update this object.")) | ||||
|         return super().validate(attrs) | ||||
|  | ||||
|  | ||||
| @ -11,7 +11,6 @@ from django.db.models.expressions import BaseExpression, Combinable | ||||
| from django.db.models.signals import post_init | ||||
| from django.http import HttpRequest | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.events.middleware import AuditMiddleware, should_log_model | ||||
| from authentik.events.utils import cleanse_dict, sanitize_item | ||||
|  | ||||
| @ -28,13 +27,10 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | ||||
|         super().connect(request) | ||||
|         if not self.enabled: | ||||
|             return | ||||
|         user = getattr(request, "user", self.anonymous_user) | ||||
|         if not user.is_authenticated: | ||||
|             user = self.anonymous_user | ||||
|         if not hasattr(request, "request_id"): | ||||
|             return | ||||
|         post_init.connect( | ||||
|             partial(self.post_init_handler, user=user, request=request), | ||||
|             partial(self.post_init_handler, request=request), | ||||
|             dispatch_uid=request.request_id, | ||||
|             weak=False, | ||||
|         ) | ||||
| @ -76,7 +72,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | ||||
|                 diff[key] = {"previous_value": value, "new_value": after.get(key)} | ||||
|         return sanitize_item(diff) | ||||
|  | ||||
|     def post_init_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_): | ||||
|     def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_): | ||||
|         """post_init django model handler""" | ||||
|         if not should_log_model(instance): | ||||
|             return | ||||
| @ -91,7 +87,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def post_save_handler( | ||||
|         self, | ||||
|         user: User, | ||||
|         request: HttpRequest, | ||||
|         sender, | ||||
|         instance: Model, | ||||
| @ -113,6 +108,4 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | ||||
|                 for field_set in ignored_field_sets: | ||||
|                     if set(diff.keys()) == set(field_set): | ||||
|                         return None | ||||
|         return super().post_save_handler( | ||||
|             user, request, sender, instance, created, thread_kwargs, **_ | ||||
|         ) | ||||
|         return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_) | ||||
|  | ||||
| @ -188,20 +188,21 @@ class LicenseKey: | ||||
|  | ||||
|     def summary(self) -> LicenseSummary: | ||||
|         """Summary of license status""" | ||||
|         has_license = License.objects.all().count() > 0 | ||||
|         last_valid = LicenseKey.last_valid_date() | ||||
|         show_admin_warning = last_valid < now() - timedelta(weeks=2) | ||||
|         show_user_warning = last_valid < now() - timedelta(weeks=4) | ||||
|         read_only = last_valid < now() - timedelta(weeks=6) | ||||
|         latest_valid = datetime.fromtimestamp(self.exp) | ||||
|         return LicenseSummary( | ||||
|             show_admin_warning=show_admin_warning, | ||||
|             show_user_warning=show_user_warning, | ||||
|             read_only=read_only, | ||||
|             show_admin_warning=show_admin_warning and has_license, | ||||
|             show_user_warning=show_user_warning and has_license, | ||||
|             read_only=read_only and has_license, | ||||
|             latest_valid=latest_valid, | ||||
|             internal_users=self.internal_users, | ||||
|             external_users=self.external_users, | ||||
|             valid=self.is_valid(), | ||||
|             has_license=License.objects.all().count() > 0, | ||||
|             has_license=has_license, | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|  | ||||
| @ -6,13 +6,13 @@ from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| from authentik.api.authorization import OwnerFilter, OwnerPermissions | ||||
| from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.enterprise.api import EnterpriseRequiredMixin | ||||
| from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer | ||||
| from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken | ||||
|  | ||||
|  | ||||
| class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer): | ||||
| @ -23,7 +23,7 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer): | ||||
|     user = GroupMemberSerializer(source="session.user", read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Endpoint | ||||
|         model = ConnectionToken | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "provider", | ||||
| @ -49,5 +49,5 @@ class ConnectionTokenViewSet( | ||||
|     filterset_fields = ["endpoint", "session__user", "provider"] | ||||
|     search_fields = ["endpoint__name", "provider__name"] | ||||
|     ordering = ["endpoint__name", "provider__name"] | ||||
|     permission_classes = [OwnerPermissions] | ||||
|     permission_classes = [OwnerSuperuserPermissions] | ||||
|     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|  | ||||
| @ -2,11 +2,14 @@ | ||||
|  | ||||
| from datetime import datetime | ||||
|  | ||||
| from django.db.models.signals import pre_save | ||||
| from django.core.cache import cache | ||||
| from django.db.models.signals import post_save, pre_save | ||||
| from django.dispatch import receiver | ||||
| from django.utils.timezone import get_current_timezone | ||||
|  | ||||
| from authentik.enterprise.license import CACHE_KEY_ENTERPRISE_LICENSE | ||||
| from authentik.enterprise.models import License | ||||
| from authentik.enterprise.tasks import enterprise_update_usage | ||||
|  | ||||
|  | ||||
| @receiver(pre_save, sender=License) | ||||
| @ -17,3 +20,10 @@ def pre_save_license(sender: type[License], instance: License, **_): | ||||
|     instance.internal_users = status.internal_users | ||||
|     instance.external_users = status.external_users | ||||
|     instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone()) | ||||
|  | ||||
|  | ||||
| @receiver(post_save, sender=License) | ||||
| def post_save_license(sender: type[License], instance: License, **_): | ||||
|     """Trigger license usage calculation when license is saved""" | ||||
|     cache.delete(CACHE_KEY_ENTERPRISE_LICENSE) | ||||
|     enterprise_update_usage.delay() | ||||
|  | ||||
| @ -12,7 +12,6 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.events.models import ( | ||||
| @ -24,6 +23,7 @@ from authentik.events.models import ( | ||||
|     TransportMode, | ||||
| ) | ||||
| from authentik.events.utils import get_user | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class NotificationTransportSerializer(ModelSerializer): | ||||
|  | ||||
| @ -21,8 +21,8 @@ from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.events.models import SystemTask, TaskStatus | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -81,7 +81,7 @@ class SystemTaskViewSet(ReadOnlyModelViewSet): | ||||
|             500: OpenApiResponse(description="Failed to retry task"), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=True, methods=["post"]) | ||||
|     @action(detail=True, methods=["POST"], permission_classes=[]) | ||||
|     def run(self, request: Request, pk=None) -> Response: | ||||
|         """Run task""" | ||||
|         task: SystemTask = self.get_object() | ||||
|  | ||||
| @ -82,26 +82,29 @@ class AuditMiddleware: | ||||
|  | ||||
|         self.anonymous_user = get_anonymous_user() | ||||
|  | ||||
|     def get_user(self, request: HttpRequest) -> User: | ||||
|         user = getattr(request, "user", self.anonymous_user) | ||||
|         if not user.is_authenticated: | ||||
|             return self.anonymous_user | ||||
|         return user | ||||
|  | ||||
|     def connect(self, request: HttpRequest): | ||||
|         """Connect signal for automatic logging""" | ||||
|         self._ensure_fallback_user() | ||||
|         user = getattr(request, "user", self.anonymous_user) | ||||
|         if not user.is_authenticated: | ||||
|             user = self.anonymous_user | ||||
|         if not hasattr(request, "request_id"): | ||||
|             return | ||||
|         post_save.connect( | ||||
|             partial(self.post_save_handler, user=user, request=request), | ||||
|             partial(self.post_save_handler, request=request), | ||||
|             dispatch_uid=request.request_id, | ||||
|             weak=False, | ||||
|         ) | ||||
|         pre_delete.connect( | ||||
|             partial(self.pre_delete_handler, user=user, request=request), | ||||
|             partial(self.pre_delete_handler, request=request), | ||||
|             dispatch_uid=request.request_id, | ||||
|             weak=False, | ||||
|         ) | ||||
|         m2m_changed.connect( | ||||
|             partial(self.m2m_changed_handler, user=user, request=request), | ||||
|             partial(self.m2m_changed_handler, request=request), | ||||
|             dispatch_uid=request.request_id, | ||||
|             weak=False, | ||||
|         ) | ||||
| @ -147,7 +150,6 @@ class AuditMiddleware: | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def post_save_handler( | ||||
|         self, | ||||
|         user: User, | ||||
|         request: HttpRequest, | ||||
|         sender, | ||||
|         instance: Model, | ||||
| @ -158,16 +160,18 @@ class AuditMiddleware: | ||||
|         """Signal handler for all object's post_save""" | ||||
|         if not should_log_model(instance): | ||||
|             return | ||||
|         user = self.get_user(request) | ||||
|  | ||||
|         action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED | ||||
|         thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) | ||||
|         thread.kwargs.update(thread_kwargs or {}) | ||||
|         thread.run() | ||||
|  | ||||
|     def pre_delete_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_): | ||||
|     def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_): | ||||
|         """Signal handler for all object's pre_delete""" | ||||
|         if not should_log_model(instance):  # pragma: no cover | ||||
|             return | ||||
|         user = self.get_user(request) | ||||
|  | ||||
|         EventNewThread( | ||||
|             EventAction.MODEL_DELETED, | ||||
| @ -176,14 +180,13 @@ class AuditMiddleware: | ||||
|             model=model_to_dict(instance), | ||||
|         ).run() | ||||
|  | ||||
|     def m2m_changed_handler( | ||||
|         self, user: User, request: HttpRequest, sender, instance: Model, action: str, **_ | ||||
|     ): | ||||
|     def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_): | ||||
|         """Signal handler for all object's m2m_changed""" | ||||
|         if action not in ["pre_add", "pre_remove", "post_clear"]: | ||||
|             return | ||||
|         if not should_log_m2m(instance): | ||||
|             return | ||||
|         user = self.get_user(request) | ||||
|  | ||||
|         EventNewThread( | ||||
|             EventAction.MODEL_UPDATED, | ||||
|  | ||||
| @ -451,6 +451,13 @@ class NotificationTransport(SerializerModel): | ||||
|  | ||||
|     def send_email(self, notification: "Notification") -> list[str]: | ||||
|         """Send notification via global email configuration""" | ||||
|         if notification.user.email.strip() == "": | ||||
|             LOGGER.info( | ||||
|                 "Discarding notification as user has no email address", | ||||
|                 user=notification.user, | ||||
|                 notification=notification, | ||||
|             ) | ||||
|             return None | ||||
|         subject_prefix = "authentik Notification: " | ||||
|         context = { | ||||
|             "key_value": { | ||||
| @ -480,7 +487,7 @@ class NotificationTransport(SerializerModel): | ||||
|             } | ||||
|         mail = TemplateEmailMessage( | ||||
|             subject=subject_prefix + context["title"], | ||||
|             to=[f"{notification.user.name} <{notification.user.email}>"], | ||||
|             to=[(notification.user.name, notification.user.email)], | ||||
|             language=notification.user.locale(), | ||||
|             template_name="email/event_notification.html", | ||||
|             template_context=context, | ||||
|  | ||||
| @ -88,8 +88,8 @@ class SystemTask(TenantTask): | ||||
|                 "duration": max(perf_counter() - self._start_precise, 0), | ||||
|                 "task_call_module": self.__module__, | ||||
|                 "task_call_func": self.__name__, | ||||
|                 "task_call_args": args, | ||||
|                 "task_call_kwargs": kwargs, | ||||
|                 "task_call_args": sanitize_item(args), | ||||
|                 "task_call_kwargs": sanitize_item(kwargs), | ||||
|                 "status": self._status, | ||||
|                 "messages": sanitize_item(self._messages), | ||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), | ||||
| @ -113,8 +113,8 @@ class SystemTask(TenantTask): | ||||
|                 "duration": max(perf_counter() - self._start_precise, 0), | ||||
|                 "task_call_module": self.__module__, | ||||
|                 "task_call_func": self.__name__, | ||||
|                 "task_call_args": args, | ||||
|                 "task_call_kwargs": kwargs, | ||||
|                 "task_call_args": sanitize_item(args), | ||||
|                 "task_call_kwargs": sanitize_item(kwargs), | ||||
|                 "status": self._status, | ||||
|                 "messages": sanitize_item(self._messages), | ||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), | ||||
|  | ||||
| @ -3,9 +3,10 @@ | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import Application | ||||
| from authentik.core.models import Application, Token, TokenIntents | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
|  | ||||
| class TestEventsMiddleware(APITestCase): | ||||
| @ -47,3 +48,30 @@ class TestEventsMiddleware(APITestCase): | ||||
|                 context__model__name="test-delete", | ||||
|             ).exists() | ||||
|         ) | ||||
|  | ||||
|     def test_create_with_api(self): | ||||
|         """Test model creation event (with API token auth)""" | ||||
|         self.client.logout() | ||||
|         token = Token.objects.create(user=self.user, intent=TokenIntents.INTENT_API, expiring=False) | ||||
|         uid = generate_id() | ||||
|         self.client.post( | ||||
|             reverse("authentik_api:application-list"), | ||||
|             data={"name": uid, "slug": uid}, | ||||
|             HTTP_AUTHORIZATION=f"Bearer {token.key}", | ||||
|         ) | ||||
|         self.assertTrue(Application.objects.filter(name=uid).exists()) | ||||
|         event = Event.objects.filter( | ||||
|             action=EventAction.MODEL_CREATED, | ||||
|             context__model__model_name="application", | ||||
|             context__model__app="authentik_core", | ||||
|             context__model__name=uid, | ||||
|         ).first() | ||||
|         self.assertIsNotNone(event) | ||||
|         self.assertEqual( | ||||
|             event.user, | ||||
|             { | ||||
|                 "pk": self.user.pk, | ||||
|                 "email": self.user.email, | ||||
|                 "username": self.user.username, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @ -15,7 +15,6 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.blueprints.v1.exporter import FlowExporter | ||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| @ -33,6 +32,7 @@ from authentik.lib.utils.file import ( | ||||
|     set_file_url, | ||||
| ) | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """flow views tests""" | ||||
|  | ||||
| from unittest.mock import MagicMock, PropertyMock, patch | ||||
| from urllib.parse import urlencode | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.test.client import RequestFactory | ||||
| @ -18,7 +19,12 @@ from authentik.flows.models import ( | ||||
| from authentik.flows.planner import FlowPlan, FlowPlanner | ||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView | ||||
| from authentik.flows.tests import FlowTestCase | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView | ||||
| from authentik.flows.views.executor import ( | ||||
|     NEXT_ARG_NAME, | ||||
|     QS_QUERY, | ||||
|     SESSION_KEY_PLAN, | ||||
|     FlowExecutorView, | ||||
| ) | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| from authentik.policies.models import PolicyBinding | ||||
| @ -121,16 +127,73 @@ class TestFlowExecutor(FlowTestCase): | ||||
|         TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     def test_invalid_flow_redirect(self): | ||||
|         """Tests that an invalid flow still redirects""" | ||||
|         """Test invalid flow with valid redirect destination""" | ||||
|         flow = create_test_flow( | ||||
|             FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|  | ||||
|         dest = "/unique-string" | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) | ||||
|         response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}") | ||||
|         response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}") | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.url, reverse("authentik_core:root-redirect")) | ||||
|         self.assertEqual(response.url, "/unique-string") | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.executor.to_stage_response", | ||||
|         TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     def test_invalid_flow_invalid_redirect(self): | ||||
|         """Test invalid flow redirect with an invalid URL""" | ||||
|         flow = create_test_flow( | ||||
|             FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|  | ||||
|         dest = "http://something.example.com/unique-string" | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) | ||||
|  | ||||
|         response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}") | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             flow, | ||||
|             component="ak-stage-access-denied", | ||||
|             error_message="Invalid next URL", | ||||
|         ) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.executor.to_stage_response", | ||||
|         TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     def test_valid_flow_redirect(self): | ||||
|         """Test valid flow with valid redirect destination""" | ||||
|         flow = create_test_flow() | ||||
|  | ||||
|         dest = "/unique-string" | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) | ||||
|  | ||||
|         response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}") | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.url, "/unique-string") | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.executor.to_stage_response", | ||||
|         TO_STAGE_RESPONSE_MOCK, | ||||
|     ) | ||||
|     def test_valid_flow_invalid_redirect(self): | ||||
|         """Test valid flow redirect with an invalid URL""" | ||||
|         flow = create_test_flow() | ||||
|  | ||||
|         dest = "http://something.example.com/unique-string" | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) | ||||
|  | ||||
|         response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}") | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertStageResponse( | ||||
|             response, | ||||
|             flow, | ||||
|             component="ak-stage-access-denied", | ||||
|             error_message="Invalid next URL", | ||||
|         ) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.executor.to_stage_response", | ||||
|  | ||||
| @ -12,6 +12,7 @@ from django.shortcuts import get_object_or_404, redirect | ||||
| from django.template.response import TemplateResponse | ||||
| from django.urls import reverse | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.decorators.clickjacking import xframe_options_sameorigin | ||||
| from django.views.generic import View | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| @ -178,6 +179,8 @@ class FlowExecutorView(APIView): | ||||
|                     self.cancel() | ||||
|                 self._logger.debug("f(exec): Continuing existing plan") | ||||
|  | ||||
|             # Initial flow request, check if we have an upstream query string passed in | ||||
|             request.session[SESSION_KEY_GET] = get_params | ||||
|             # Don't check session again as we've either already loaded the plan or we need to plan | ||||
|             if not self.plan: | ||||
|                 request.session[SESSION_KEY_HISTORY] = [] | ||||
| @ -192,8 +195,6 @@ class FlowExecutorView(APIView): | ||||
|                     # To match behaviour with loading an empty flow plan from cache, | ||||
|                     # we don't show an error message here, but rather call _flow_done() | ||||
|                     return self._flow_done() | ||||
|             # Initial flow request, check if we have an upstream query string passed in | ||||
|             request.session[SESSION_KEY_GET] = get_params | ||||
|             # We don't save the Plan after getting the next stage | ||||
|             # as it hasn't been successfully passed yet | ||||
|             try: | ||||
| @ -392,7 +393,11 @@ class FlowExecutorView(APIView): | ||||
|             NEXT_ARG_NAME, "authentik_core:root-redirect" | ||||
|         ) | ||||
|         self.cancel() | ||||
|         if next_param and not is_url_absolute(next_param): | ||||
|             return to_stage_response(self.request, redirect_with_qs(next_param)) | ||||
|         return to_stage_response( | ||||
|             self.request, self.stage_invalid(error_message=_("Invalid next URL")) | ||||
|         ) | ||||
|  | ||||
|     def stage_ok(self) -> HttpResponse: | ||||
|         """Callback called by stages upon successful completion. | ||||
|  | ||||
| @ -13,7 +13,6 @@ from rest_framework.viewsets import GenericViewSet | ||||
| from structlog.stdlib import get_logger | ||||
| from structlog.testing import capture_logs | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.applications import user_app_cache_key | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer | ||||
| @ -23,6 +22,7 @@ from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSe | ||||
| from authentik.policies.models import Policy, PolicyBinding | ||||
| from authentik.policies.process import PolicyProcess | ||||
| from authentik.policies.types import CACHE_PREFIX, PolicyRequest | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -15,13 +15,13 @@ from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer | ||||
| from authentik.core.models import Provider | ||||
| from authentik.providers.oauth2.id_token import IDToken | ||||
| from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class OAuth2ProviderSerializer(ProviderSerializer): | ||||
|  | ||||
| @ -36,8 +36,21 @@ class TestAuthorize(OAuthTestCase): | ||||
|  | ||||
|     def test_invalid_grant_type(self): | ||||
|         """Test with invalid grant type""" | ||||
|         OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris="http://local.invalid/Foo", | ||||
|         ) | ||||
|         with self.assertRaises(AuthorizeError): | ||||
|             request = self.factory.get("/", data={"response_type": "invalid"}) | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
|                     "response_type": "invalid", | ||||
|                     "client_id": "test", | ||||
|                     "redirect_uri": "http://local.invalid/Foo", | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|  | ||||
|     def test_invalid_client_id(self): | ||||
| @ -344,7 +357,12 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 ] | ||||
|             ) | ||||
|         ) | ||||
|         Application.objects.create(name="app", slug="app", provider=provider) | ||||
|         provider.property_mappings.add( | ||||
|             ScopeMapping.objects.create( | ||||
|                 name=generate_id(), scope_name="test", expression="""return {"sub": "foo"}""" | ||||
|             ) | ||||
|         ) | ||||
|         Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||
|         state = generate_id() | ||||
|         user = create_test_admin_user() | ||||
|         self.client.force_login(user) | ||||
| @ -365,7 +383,7 @@ class TestAuthorize(OAuthTestCase): | ||||
|                     "response_type": "id_token", | ||||
|                     "client_id": "test", | ||||
|                     "state": state, | ||||
|                     "scope": "openid", | ||||
|                     "scope": "openid test", | ||||
|                     "redirect_uri": "http://localhost", | ||||
|                     "nonce": generate_id(), | ||||
|                 }, | ||||
| @ -390,6 +408,7 @@ class TestAuthorize(OAuthTestCase): | ||||
|             ) | ||||
|             jwt = self.validate_jwt(token, provider) | ||||
|             self.assertEqual(jwt["amr"], ["pwd"]) | ||||
|             self.assertEqual(jwt["sub"], "foo") | ||||
|             self.assertAlmostEqual( | ||||
|                 jwt["exp"] - now().timestamp(), | ||||
|                 expires, | ||||
|  | ||||
| @ -4,9 +4,10 @@ from urllib.parse import urlencode | ||||
|  | ||||
| from django.urls import reverse | ||||
|  | ||||
| from authentik.core.models import Application | ||||
| from authentik.core.models import Application, Group | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | ||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||
| from authentik.providers.oauth2.views.device_init import QS_KEY_CODE | ||||
| @ -77,3 +78,23 @@ class TesOAuth2DeviceInit(OAuthTestCase): | ||||
|             + "?" | ||||
|             + urlencode({QS_KEY_CODE: token.user_code}), | ||||
|         ) | ||||
|  | ||||
|     def test_device_init_denied(self): | ||||
|         """Test device init""" | ||||
|         group = Group.objects.create(name="foo") | ||||
|         PolicyBinding.objects.create( | ||||
|             group=group, | ||||
|             target=self.application, | ||||
|             order=0, | ||||
|         ) | ||||
|         token = DeviceToken.objects.create( | ||||
|             user_code="foo", | ||||
|             provider=self.provider, | ||||
|         ) | ||||
|         res = self.client.get( | ||||
|             reverse("authentik_providers_oauth2_root:device-login") | ||||
|             + "?" | ||||
|             + urlencode({QS_KEY_CODE: token.user_code}) | ||||
|         ) | ||||
|         self.assertEqual(res.status_code, 200) | ||||
|         self.assertIn(b"Permission denied", res.content) | ||||
|  | ||||
| @ -23,7 +23,7 @@ from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||
| 
 | ||||
| 
 | ||||
| class TestTokenClientCredentialsUserNamePassword(OAuthTestCase): | ||||
| class TestTokenClientCredentials(OAuthTestCase): | ||||
|     """Test token (client_credentials) view""" | ||||
| 
 | ||||
|     @apply_blueprint("system/providers-oauth2.yaml") | ||||
| @ -1,170 +0,0 @@ | ||||
| """Test token view""" | ||||
|  | ||||
| from json import loads | ||||
|  | ||||
| from django.test import RequestFactory | ||||
| from django.urls import reverse | ||||
| from jwt import decode | ||||
|  | ||||
| from authentik.blueprints.tests import apply_blueprint | ||||
| from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.providers.oauth2.constants import ( | ||||
|     GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|     GRANT_TYPE_PASSWORD, | ||||
|     SCOPE_OPENID, | ||||
|     SCOPE_OPENID_EMAIL, | ||||
|     SCOPE_OPENID_PROFILE, | ||||
|     TOKEN_TYPE, | ||||
| ) | ||||
| from authentik.providers.oauth2.errors import TokenError | ||||
| from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||
|  | ||||
|  | ||||
| class TestTokenClientCredentialsStandard(OAuthTestCase): | ||||
|     """Test token (client_credentials) view""" | ||||
|  | ||||
|     @apply_blueprint("system/providers-oauth2.yaml") | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.factory = RequestFactory() | ||||
|         self.provider = OAuth2Provider.objects.create( | ||||
|             name="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris="http://testserver", | ||||
|             signing_key=create_test_cert(), | ||||
|         ) | ||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||
|         self.app = Application.objects.create(name="test", slug="test", provider=self.provider) | ||||
|         self.user = create_test_admin_user("sa") | ||||
|         self.user.type = UserTypes.SERVICE_ACCOUNT | ||||
|         self.user.save() | ||||
|         self.token = Token.objects.create( | ||||
|             identifier="sa-token", | ||||
|             user=self.user, | ||||
|             intent=TokenIntents.INTENT_APP_PASSWORD, | ||||
|             expiring=False, | ||||
|         ) | ||||
|  | ||||
|     def test_wrong_user(self): | ||||
|         """test invalid username""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": self.provider.client_secret + "foo", | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_no_provider(self): | ||||
|         """test no provider""" | ||||
|         self.app.provider = None | ||||
|         self.app.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": self.provider.client_secret, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_permission_denied(self): | ||||
|         """test permission denied""" | ||||
|         group = Group.objects.create(name="foo") | ||||
|         PolicyBinding.objects.create( | ||||
|             group=group, | ||||
|             target=self.app, | ||||
|             order=0, | ||||
|         ) | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": self.provider.client_secret, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_successful(self): | ||||
|         """test successful""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": self.provider.client_secret, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["token_type"], TOKEN_TYPE) | ||||
|         _, alg = self.provider.jwt_key | ||||
|         jwt = decode( | ||||
|             body["access_token"], | ||||
|             key=self.provider.signing_key.public_key, | ||||
|             algorithms=[alg], | ||||
|             audience=self.provider.client_id, | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             jwt["given_name"], "Autogenerated user from application test (client credentials)" | ||||
|         ) | ||||
|         self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") | ||||
|         jwt = decode( | ||||
|             body["id_token"], | ||||
|             key=self.provider.signing_key.public_key, | ||||
|             algorithms=[alg], | ||||
|             audience=self.provider.client_id, | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             jwt["given_name"], "Autogenerated user from application test (client credentials)" | ||||
|         ) | ||||
|         self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") | ||||
|  | ||||
|     def test_successful_password(self): | ||||
|         """test successful (password grant)""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_PASSWORD, | ||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": self.provider.client_secret, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["token_type"], TOKEN_TYPE) | ||||
|         _, alg = self.provider.jwt_key | ||||
|         jwt = decode( | ||||
|             body["access_token"], | ||||
|             key=self.provider.signing_key.public_key, | ||||
|             algorithms=[alg], | ||||
|             audience=self.provider.client_id, | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             jwt["given_name"], "Autogenerated user from application test (client credentials)" | ||||
|         ) | ||||
|         self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") | ||||
| @ -1,182 +0,0 @@ | ||||
| """Test token view""" | ||||
|  | ||||
| from base64 import b64encode | ||||
| from json import loads | ||||
|  | ||||
| from django.test import RequestFactory | ||||
| from django.urls import reverse | ||||
| from jwt import decode | ||||
|  | ||||
| from authentik.blueprints.tests import apply_blueprint | ||||
| from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.providers.oauth2.constants import ( | ||||
|     GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|     GRANT_TYPE_PASSWORD, | ||||
|     SCOPE_OPENID, | ||||
|     SCOPE_OPENID_EMAIL, | ||||
|     SCOPE_OPENID_PROFILE, | ||||
|     TOKEN_TYPE, | ||||
| ) | ||||
| from authentik.providers.oauth2.errors import TokenError | ||||
| from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||
|  | ||||
|  | ||||
| class TestTokenClientCredentialsStandardCompat(OAuthTestCase): | ||||
|     """Test token (client_credentials) view""" | ||||
|  | ||||
|     @apply_blueprint("system/providers-oauth2.yaml") | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.factory = RequestFactory() | ||||
|         self.provider = OAuth2Provider.objects.create( | ||||
|             name="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris="http://testserver", | ||||
|             signing_key=create_test_cert(), | ||||
|         ) | ||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||
|         self.app = Application.objects.create(name="test", slug="test", provider=self.provider) | ||||
|         self.user = create_test_admin_user("sa") | ||||
|         self.user.type = UserTypes.SERVICE_ACCOUNT | ||||
|         self.user.save() | ||||
|         self.token = Token.objects.create( | ||||
|             identifier="sa-token", | ||||
|             user=self.user, | ||||
|             intent=TokenIntents.INTENT_APP_PASSWORD, | ||||
|             expiring=False, | ||||
|         ) | ||||
|  | ||||
|     def test_wrong_user(self): | ||||
|         """test invalid username""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": b64encode(f"saa:{self.token.key}".encode()).decode(), | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_wrong_token(self): | ||||
|         """test invalid token""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": b64encode(f"sa:{self.token.key}foo".encode()).decode(), | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_no_provider(self): | ||||
|         """test no provider""" | ||||
|         self.app.provider = None | ||||
|         self.app.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(), | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_permission_denied(self): | ||||
|         """test permission denied""" | ||||
|         group = Group.objects.create(name="foo") | ||||
|         PolicyBinding.objects.create( | ||||
|             group=group, | ||||
|             target=self.app, | ||||
|             order=0, | ||||
|         ) | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": SCOPE_OPENID, | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(), | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertJSONEqual( | ||||
|             response.content.decode(), | ||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||
|         ) | ||||
|  | ||||
|     def test_successful(self): | ||||
|         """test successful""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, | ||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(), | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["token_type"], TOKEN_TYPE) | ||||
|         _, alg = self.provider.jwt_key | ||||
|         jwt = decode( | ||||
|             body["access_token"], | ||||
|             key=self.provider.signing_key.public_key, | ||||
|             algorithms=[alg], | ||||
|             audience=self.provider.client_id, | ||||
|         ) | ||||
|         self.assertEqual(jwt["given_name"], self.user.name) | ||||
|         self.assertEqual(jwt["preferred_username"], self.user.username) | ||||
|         jwt = decode( | ||||
|             body["id_token"], | ||||
|             key=self.provider.signing_key.public_key, | ||||
|             algorithms=[alg], | ||||
|             audience=self.provider.client_id, | ||||
|         ) | ||||
|         self.assertEqual(jwt["given_name"], self.user.name) | ||||
|         self.assertEqual(jwt["preferred_username"], self.user.username) | ||||
|  | ||||
|     def test_successful_password(self): | ||||
|         """test successful (password grant)""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             { | ||||
|                 "grant_type": GRANT_TYPE_PASSWORD, | ||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}", | ||||
|                 "client_id": self.provider.client_id, | ||||
|                 "client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(), | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["token_type"], TOKEN_TYPE) | ||||
|         _, alg = self.provider.jwt_key | ||||
|         jwt = decode( | ||||
|             body["access_token"], | ||||
|             key=self.provider.signing_key.public_key, | ||||
|             algorithms=[alg], | ||||
|             audience=self.provider.client_id, | ||||
|         ) | ||||
|         self.assertEqual(jwt["given_name"], self.user.name) | ||||
|         self.assertEqual(jwt["preferred_username"], self.user.username) | ||||
| @ -121,44 +121,18 @@ class OAuthAuthorizationParams: | ||||
|         redirect_uri = query_dict.get("redirect_uri", "") | ||||
|  | ||||
|         response_type = query_dict.get("response_type", "") | ||||
|         grant_type = None | ||||
|         # Determine which flow to use. | ||||
|         if response_type in [ResponseTypes.CODE]: | ||||
|             grant_type = GrantTypes.AUTHORIZATION_CODE | ||||
|         elif response_type in [ | ||||
|             ResponseTypes.ID_TOKEN, | ||||
|             ResponseTypes.ID_TOKEN_TOKEN, | ||||
|         ]: | ||||
|             grant_type = GrantTypes.IMPLICIT | ||||
|         elif response_type in [ | ||||
|             ResponseTypes.CODE_TOKEN, | ||||
|             ResponseTypes.CODE_ID_TOKEN, | ||||
|             ResponseTypes.CODE_ID_TOKEN_TOKEN, | ||||
|         ]: | ||||
|             grant_type = GrantTypes.HYBRID | ||||
|  | ||||
|         # Grant type validation. | ||||
|         if not grant_type: | ||||
|             LOGGER.warning("Invalid response type", type=response_type) | ||||
|             raise AuthorizeError(redirect_uri, "unsupported_response_type", "", state) | ||||
|  | ||||
|         # Validate and check the response_mode against the predefined dict | ||||
|         # Set to Query or Fragment if not defined in request | ||||
|         response_mode = query_dict.get("response_mode", False) | ||||
|  | ||||
|         if response_mode not in ResponseMode.values: | ||||
|             response_mode = ResponseMode.QUERY | ||||
|  | ||||
|             if grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: | ||||
|                 response_mode = ResponseMode.FRAGMENT | ||||
|  | ||||
|         max_age = query_dict.get("max_age") | ||||
|         return OAuthAuthorizationParams( | ||||
|             client_id=query_dict.get("client_id", ""), | ||||
|             redirect_uri=redirect_uri, | ||||
|             response_type=response_type, | ||||
|             response_mode=response_mode, | ||||
|             grant_type=grant_type, | ||||
|             grant_type="", | ||||
|             scope=set(query_dict.get("scope", "").split()), | ||||
|             state=state, | ||||
|             nonce=query_dict.get("nonce"), | ||||
| @ -178,6 +152,7 @@ class OAuthAuthorizationParams: | ||||
|             LOGGER.warning("Invalid client identifier", client_id=self.client_id) | ||||
|             raise ClientIdError(client_id=self.client_id) | ||||
|         self.check_redirect_uri() | ||||
|         self.check_grant() | ||||
|         self.check_scope(github_compat) | ||||
|         self.check_nonce() | ||||
|         self.check_code_challenge() | ||||
| @ -186,6 +161,34 @@ class OAuthAuthorizationParams: | ||||
|                 self.redirect_uri, "request_not_supported", self.grant_type, self.state | ||||
|             ) | ||||
|  | ||||
|     def check_grant(self): | ||||
|         """Check grant""" | ||||
|         # Determine which flow to use. | ||||
|         if self.response_type in [ResponseTypes.CODE]: | ||||
|             self.grant_type = GrantTypes.AUTHORIZATION_CODE | ||||
|         elif self.response_type in [ | ||||
|             ResponseTypes.ID_TOKEN, | ||||
|             ResponseTypes.ID_TOKEN_TOKEN, | ||||
|         ]: | ||||
|             self.grant_type = GrantTypes.IMPLICIT | ||||
|         elif self.response_type in [ | ||||
|             ResponseTypes.CODE_TOKEN, | ||||
|             ResponseTypes.CODE_ID_TOKEN, | ||||
|             ResponseTypes.CODE_ID_TOKEN_TOKEN, | ||||
|         ]: | ||||
|             self.grant_type = GrantTypes.HYBRID | ||||
|  | ||||
|         # Grant type validation. | ||||
|         if not self.grant_type: | ||||
|             LOGGER.warning("Invalid response type", type=self.response_type) | ||||
|             raise AuthorizeError(self.redirect_uri, "unsupported_response_type", "", self.state) | ||||
|  | ||||
|         if self.response_mode not in ResponseMode.values: | ||||
|             self.response_mode = ResponseMode.QUERY | ||||
|  | ||||
|             if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: | ||||
|                 self.response_mode = ResponseMode.FRAGMENT | ||||
|  | ||||
|     def check_redirect_uri(self): | ||||
|         """Redirect URI validation.""" | ||||
|         allowed_redirect_urls = self.provider.redirect_uris.split() | ||||
| @ -257,9 +260,9 @@ class OAuthAuthorizationParams: | ||||
|         if SCOPE_OFFLINE_ACCESS in self.scope: | ||||
|             # https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess | ||||
|             if PROMPT_CONSENT not in self.prompt: | ||||
|                 raise AuthorizeError( | ||||
|                     self.redirect_uri, "consent_required", self.grant_type, self.state | ||||
|                 ) | ||||
|                 # Instead of ignoring the `offline_access` scope when `prompt` | ||||
|                 # isn't set to `consent`, we set override it ourselves | ||||
|                 self.prompt.add(PROMPT_CONSENT) | ||||
|             if self.response_type not in [ | ||||
|                 ResponseTypes.CODE, | ||||
|                 ResponseTypes.CODE_TOKEN, | ||||
|  | ||||
| @ -12,10 +12,11 @@ from django.views.decorators.csrf import csrf_exempt | ||||
| from rest_framework.throttling import AnonRateThrottle | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import Application | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | ||||
| from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application | ||||
| from authentik.providers.oauth2.views.device_init import QS_KEY_CODE | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -38,7 +39,9 @@ class DeviceView(View): | ||||
|         ).first() | ||||
|         if not provider: | ||||
|             return HttpResponseBadRequest() | ||||
|         if not get_application(provider): | ||||
|         try: | ||||
|             _ = provider.application | ||||
|         except Application.DoesNotExist: | ||||
|             return HttpResponseBadRequest() | ||||
|         self.provider = provider | ||||
|         self.client_id = client_id | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| """Device flow views""" | ||||
|  | ||||
| from typing import Optional | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views import View | ||||
| from rest_framework.exceptions import ErrorDetail | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.fields import CharField, IntegerField | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| @ -18,6 +17,7 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.urls import redirect_with_qs | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | ||||
| from authentik.providers.oauth2.views.device_finish import ( | ||||
|     PLAN_CONTEXT_DEVICE, | ||||
| @ -44,32 +44,36 @@ def get_application(provider: OAuth2Provider) -> Optional[Application]: | ||||
|         return None | ||||
|  | ||||
|  | ||||
| def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]: | ||||
|     """Validate user token""" | ||||
|     token = DeviceToken.objects.filter( | ||||
|         user_code=code, | ||||
|     ).first() | ||||
|     if not token: | ||||
|         return None | ||||
| class CodeValidatorView(PolicyAccessView): | ||||
|     """Helper to validate frontside token""" | ||||
|  | ||||
|     app = get_application(token.provider) | ||||
|     if not app: | ||||
|         return None | ||||
|     def __init__(self, code: str, **kwargs: Any) -> None: | ||||
|         super().__init__(**kwargs) | ||||
|         self.code = code | ||||
|  | ||||
|     scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider) | ||||
|     planner = FlowPlanner(token.provider.authorization_flow) | ||||
|     def resolve_provider_application(self): | ||||
|         self.token = DeviceToken.objects.filter(user_code=self.code).first() | ||||
|         if not self.token: | ||||
|             raise Application.DoesNotExist | ||||
|         self.provider = self.token.provider | ||||
|         self.application = self.token.provider.application | ||||
|  | ||||
|     def get(self, request: HttpRequest, *args, **kwargs): | ||||
|         scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider) | ||||
|         planner = FlowPlanner(self.provider.authorization_flow) | ||||
|         planner.allow_empty_flows = True | ||||
|         planner.use_cache = False | ||||
|         try: | ||||
|             plan = planner.plan( | ||||
|                 request, | ||||
|                 { | ||||
|                     PLAN_CONTEXT_SSO: True, | ||||
|                 PLAN_CONTEXT_APPLICATION: app, | ||||
|                     PLAN_CONTEXT_APPLICATION: self.application, | ||||
|                     # OAuth2 related params | ||||
|                 PLAN_CONTEXT_DEVICE: token, | ||||
|                     PLAN_CONTEXT_DEVICE: self.token, | ||||
|                     # Consent related params | ||||
|                     PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.") | ||||
|                 % {"application": app.name}, | ||||
|                     % {"application": self.application.name}, | ||||
|                     PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions, | ||||
|                 }, | ||||
|             ) | ||||
| @ -81,11 +85,11 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]: | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             request.GET, | ||||
|         flow_slug=token.provider.authorization_flow.slug, | ||||
|             flow_slug=self.token.provider.authorization_flow.slug, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class DeviceEntryView(View): | ||||
| class DeviceEntryView(PolicyAccessView): | ||||
|     """View used to initiate the device-code flow, url entered by endusers""" | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
| @ -95,7 +99,9 @@ class DeviceEntryView(View): | ||||
|             LOGGER.info("Brand has no device code flow configured", brand=brand) | ||||
|             return HttpResponse(status=404) | ||||
|         if QS_KEY_CODE in request.GET: | ||||
|             validation = validate_code(request.GET[QS_KEY_CODE], request) | ||||
|             validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch( | ||||
|                 request | ||||
|             ) | ||||
|             if validation: | ||||
|                 return validation | ||||
|             LOGGER.info("Got code from query parameter but no matching token found") | ||||
| @ -130,6 +136,13 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse): | ||||
|     code = IntegerField() | ||||
|     component = CharField(default="ak-provider-oauth2-device-code") | ||||
|  | ||||
|     def validate_code(self, code: int) -> HttpResponse | None: | ||||
|         """Validate code and save the returned http response""" | ||||
|         response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request) | ||||
|         if not response: | ||||
|             raise ValidationError(_("Invalid code"), "invalid") | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class OAuthDeviceCodeStage(ChallengeStageView): | ||||
|     """Flow challenge for users to enter device codes""" | ||||
| @ -145,12 +158,4 @@ class OAuthDeviceCodeStage(ChallengeStageView): | ||||
|         ) | ||||
|  | ||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||
|         code = response.validated_data["code"] | ||||
|         validation = validate_code(code, self.request) | ||||
|         if not validation: | ||||
|             response._errors.setdefault("code", []) | ||||
|             response._errors["code"].append(ErrorDetail(_("Invalid code"), "invalid")) | ||||
|             return self.challenge_invalid(response) | ||||
|         # Run cancel to cleanup the current flow | ||||
|         self.executor.cancel() | ||||
|         return validation | ||||
|         return response.validated_data["code"] | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| """authentik OAuth2 Token views""" | ||||
|  | ||||
| from base64 import b64decode, urlsafe_b64encode | ||||
| from binascii import Error | ||||
| from base64 import urlsafe_b64encode | ||||
| from dataclasses import InitVar, dataclass | ||||
| from datetime import datetime | ||||
| from hashlib import sha256 | ||||
| @ -24,12 +23,10 @@ from authentik.core.middleware import CTX_AUTH_VIA | ||||
| from authentik.core.models import ( | ||||
|     USER_ATTRIBUTE_EXPIRES, | ||||
|     USER_ATTRIBUTE_GENERATED, | ||||
|     USER_PATH_SYSTEM_PREFIX, | ||||
|     Application, | ||||
|     Token, | ||||
|     TokenIntents, | ||||
|     User, | ||||
|     UserTypes, | ||||
| ) | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.signals import get_login_event | ||||
| @ -289,29 +286,11 @@ class TokenParams: | ||||
|             raise TokenError("invalid_grant") | ||||
|  | ||||
|     def __post_init_client_credentials(self, request: HttpRequest): | ||||
|         # client_credentials flow with client assertion | ||||
|         if request.POST.get(CLIENT_ASSERTION_TYPE, "") != "": | ||||
|             return self.__post_init_client_credentials_jwt(request) | ||||
|         # authentik-custom-ish client credentials flow | ||||
|         if request.POST.get("username", "") != "": | ||||
|             return self.__post_init_client_credentials_creds( | ||||
|                 request, request.POST.get("username"), request.POST.get("password") | ||||
|             ) | ||||
|         # Standard method which creates an automatic user | ||||
|         if self.client_secret == self.provider.client_secret: | ||||
|             return self.__post_init_client_credentials_generated(request) | ||||
|         # Standard workaround method which stores username:password | ||||
|         # as client_secret | ||||
|         try: | ||||
|             user, _, password = b64decode(self.client_secret).decode("utf-8").partition(":") | ||||
|             return self.__post_init_client_credentials_creds(request, user, password) | ||||
|         except (ValueError, Error): | ||||
|             raise TokenError("invalid_grant") | ||||
|  | ||||
|     def __post_init_client_credentials_creds( | ||||
|         self, request: HttpRequest, username: str, password: str | ||||
|     ): | ||||
|         # Authenticate user based on credentials | ||||
|         username = request.POST.get("username") | ||||
|         password = request.POST.get("password") | ||||
|         user = User.objects.filter(username=username).first() | ||||
|         if not user: | ||||
|             raise TokenError("invalid_grant") | ||||
| @ -337,6 +316,7 @@ class TokenParams: | ||||
|                 PLAN_CONTEXT_APPLICATION: app, | ||||
|             }, | ||||
|         ).from_http(request, user=user) | ||||
|         return None | ||||
|  | ||||
|     # pylint: disable=too-many-locals | ||||
|     def __post_init_client_credentials_jwt(self, request: HttpRequest): | ||||
| @ -429,35 +409,6 @@ class TokenParams: | ||||
|             }, | ||||
|         ).from_http(request, user=self.user) | ||||
|  | ||||
|     def __post_init_client_credentials_generated(self, request: HttpRequest): | ||||
|         # Authorize user access | ||||
|         app = Application.objects.filter(provider=self.provider).first() | ||||
|         if not app or not app.provider: | ||||
|             raise TokenError("invalid_grant") | ||||
|         self.user, _ = User.objects.update_or_create( | ||||
|             # trim username to ensure the entire username is max 150 chars | ||||
|             # (22 chars being the length of the "template") | ||||
|             username=f"ak-{self.provider.name[:150-22]}-client_credentials", | ||||
|             defaults={ | ||||
|                 "attributes": { | ||||
|                     USER_ATTRIBUTE_GENERATED: True, | ||||
|                 }, | ||||
|                 "last_login": timezone.now(), | ||||
|                 "name": f"Autogenerated user from application {app.name} (client credentials)", | ||||
|                 "path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}", | ||||
|                 "type": UserTypes.SERVICE_ACCOUNT, | ||||
|             }, | ||||
|         ) | ||||
|         self.__check_policy_access(app, request) | ||||
|  | ||||
|         Event.new( | ||||
|             action=EventAction.LOGIN, | ||||
|             **{ | ||||
|                 PLAN_CONTEXT_METHOD: "oauth_client_secret", | ||||
|                 PLAN_CONTEXT_APPLICATION: app, | ||||
|             }, | ||||
|         ).from_http(request, user=self.user) | ||||
|  | ||||
|     def __post_init_device_code(self, request: HttpRequest): | ||||
|         device_code = request.POST.get("device_code", "") | ||||
|         code = DeviceToken.objects.filter(device_code=device_code, provider=self.provider).first() | ||||
| @ -467,6 +418,7 @@ class TokenParams: | ||||
|  | ||||
|     def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource): | ||||
|         """Create user from JWT""" | ||||
|         exp = token.get("exp") | ||||
|         self.user, created = User.objects.update_or_create( | ||||
|             username=f"{self.provider.name}-{token.get('sub')}", | ||||
|             defaults={ | ||||
| @ -476,10 +428,8 @@ class TokenParams: | ||||
|                 "last_login": timezone.now(), | ||||
|                 "name": f"Autogenerated user from application {app.name} (client credentials JWT)", | ||||
|                 "path": source.get_user_path(), | ||||
|                 "type": UserTypes.SERVICE_ACCOUNT, | ||||
|             }, | ||||
|         ) | ||||
|         exp = token.get("exp") | ||||
|         if created and exp: | ||||
|             self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp | ||||
|             self.user.save() | ||||
|  | ||||
| @ -101,8 +101,8 @@ class UserInfoView(View): | ||||
|                     value=value, | ||||
|                 ) | ||||
|                 continue | ||||
|             LOGGER.debug("updated scope", scope=scope) | ||||
|             always_merger.merge(final_claims, value) | ||||
|             LOGGER.debug("updated scope", scope=scope) | ||||
|         return final_claims | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
| @ -121,8 +121,9 @@ class UserInfoView(View): | ||||
|         """Handle GET Requests for UserInfo""" | ||||
|         if not self.token: | ||||
|             return HttpResponseBadRequest() | ||||
|         claims = self.get_claims(self.token.provider, self.token) | ||||
|         claims["sub"] = self.token.id_token.sub | ||||
|         claims = {} | ||||
|         claims.setdefault("sub", self.token.id_token.sub) | ||||
|         claims.update(self.get_claims(self.token.provider, self.token)) | ||||
|         if self.token.id_token.nonce: | ||||
|             claims["nonce"] = self.token.id_token.nonce | ||||
|         response = TokenResponse(claims) | ||||
|  | ||||
| @ -22,7 +22,6 @@ from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer | ||||
| @ -33,6 +32,7 @@ from authentik.providers.saml.processors.assertion import AssertionProcessor | ||||
| from authentik.providers.saml.processors.authn_request_parser import AuthNRequest | ||||
| from authentik.providers.saml.processors.metadata import MetadataProcessor | ||||
| from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -15,10 +15,10 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.policies.event_matcher.models import model_choices | ||||
| from authentik.rbac.api.rbac import PermissionAssignSerializer | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.rbac.models import Role | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -16,11 +16,11 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.models import User, UserTypes | ||||
| from authentik.policies.event_matcher.models import model_choices | ||||
| from authentik.rbac.api.rbac import PermissionAssignSerializer | ||||
| from authentik.rbac.decorators import permission_required | ||||
|  | ||||
|  | ||||
| class UserObjectPermissionSerializer(ModelSerializer): | ||||
|  | ||||
| @ -14,18 +14,23 @@ LOGGER = get_logger() | ||||
| def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[list[str]] = None): | ||||
|     """Check permissions for a single custom action""" | ||||
| 
 | ||||
|     def wrapper_outter(func: Callable): | ||||
|     def _check_obj_perm(self: ModelViewSet, request: Request): | ||||
|         # Check obj_perm both globally and on the specific object | ||||
|         # Having the global permission has higher priority | ||||
|         if request.user.has_perm(obj_perm): | ||||
|             return | ||||
|         obj = self.get_object() | ||||
|         if not request.user.has_perm(obj_perm, obj): | ||||
|             LOGGER.debug("denying access for object", user=request.user, perm=obj_perm, obj=obj) | ||||
|             self.permission_denied(request) | ||||
| 
 | ||||
|     def wrapper_outer(func: Callable): | ||||
|         """Check permissions for a single custom action""" | ||||
| 
 | ||||
|         @wraps(func) | ||||
|         def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: | ||||
|             if obj_perm: | ||||
|                 obj = self.get_object() | ||||
|                 if not request.user.has_perm(obj_perm, obj): | ||||
|                     LOGGER.debug( | ||||
|                         "denying access for object", user=request.user, perm=obj_perm, obj=obj | ||||
|                     ) | ||||
|                     return self.permission_denied(request) | ||||
|                 _check_obj_perm(self, request) | ||||
|             if global_perms: | ||||
|                 for other_perm in global_perms: | ||||
|                     if not request.user.has_perm(other_perm): | ||||
| @ -35,4 +40,4 @@ def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[l | ||||
| 
 | ||||
|         return wrapper | ||||
| 
 | ||||
|     return wrapper_outter | ||||
|     return wrapper_outer | ||||
							
								
								
									
										58
									
								
								authentik/rbac/tests/test_decorators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								authentik/rbac/tests/test_decorators.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| """test decorators api""" | ||||
|  | ||||
| from django.urls import reverse | ||||
| from guardian.shortcuts import assign_perm | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import Application | ||||
| from authentik.core.tests.utils import create_test_user | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
|  | ||||
| class TestAPIDecorators(APITestCase): | ||||
|     """test decorators api""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.user = create_test_user() | ||||
|  | ||||
|     def test_obj_perm_denied(self): | ||||
|         """Test object perm denied""" | ||||
|         self.client.force_login(self.user) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 403) | ||||
|  | ||||
|     def test_obj_perm_global(self): | ||||
|         """Test object perm successful (global)""" | ||||
|         assign_perm("authentik_core.view_application", self.user) | ||||
|         assign_perm("authentik_events.view_event", self.user) | ||||
|         self.client.force_login(self.user) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_obj_perm_scoped(self): | ||||
|         """Test object perm successful (scoped)""" | ||||
|         assign_perm("authentik_events.view_event", self.user) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||
|         assign_perm("authentik_core.view_application", self.user, app) | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_other_perm_denied(self): | ||||
|         """Test other perm denied""" | ||||
|         self.client.force_login(self.user) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||
|         assign_perm("authentik_core.view_application", self.user, app) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 403) | ||||
| @ -7,6 +7,8 @@ from psycopg import connect | ||||
|  | ||||
| from authentik.lib.config import CONFIG | ||||
|  | ||||
| QUERY = """SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;""" | ||||
|  | ||||
|  | ||||
| @lru_cache | ||||
| def get_install_id() -> str: | ||||
| @ -18,7 +20,7 @@ def get_install_id() -> str: | ||||
|     if settings.TEST: | ||||
|         return str(uuid4()) | ||||
|     with connection.cursor() as cursor: | ||||
|         cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;") | ||||
|         cursor.execute(QUERY) | ||||
|         return cursor.fetchone()[0] | ||||
|  | ||||
|  | ||||
| @ -38,5 +40,5 @@ def get_install_id_raw(): | ||||
|         sslkey=CONFIG.get("postgresql.sslkey"), | ||||
|     ) | ||||
|     cursor = conn.cursor() | ||||
|     cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;") | ||||
|     cursor.execute(QUERY) | ||||
|     return cursor.fetchone()[0] | ||||
|  | ||||
| @ -13,12 +13,12 @@ from rest_framework.serializers import ValidationError | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.sources import SourceSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.flows.challenge import RedirectChallenge | ||||
| from authentik.flows.views.executor import to_stage_response | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.sources.plex.models import PlexSource, PlexSourceConnection | ||||
| from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager | ||||
|  | ||||
|  | ||||
| @ -17,9 +17,9 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.authorization import OwnerFilter, OwnerPermissions | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.flows.api.stages import StageSerializer | ||||
| from authentik.rbac.decorators import permission_required | ||||
| from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice | ||||
| from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL | ||||
| from authentik.stages.authenticator_duo.tasks import duo_import_devices | ||||
|  | ||||
| @ -65,7 +65,7 @@ def get_webauthn_challenge_without_user( | ||||
|     authentication_options = generate_authentication_options( | ||||
|         rp_id=get_rp_id(request), | ||||
|         allow_credentials=[], | ||||
|         user_verification=stage.webauthn_user_verification, | ||||
|         user_verification=UserVerificationRequirement(stage.webauthn_user_verification), | ||||
|     ) | ||||
|     request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge | ||||
|  | ||||
|  | ||||
| @ -164,8 +164,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | ||||
|         """Test webauthn (userless)""" | ||||
|         request = get_request("/") | ||||
|         stage = AuthenticatorValidateStage.objects.create( | ||||
|             name=generate_id(), | ||||
|             name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED | ||||
|         ) | ||||
|         stage.refresh_from_db() | ||||
|         WebAuthnDevice.objects.create( | ||||
|             user=self.user, | ||||
|             public_key=( | ||||
|  | ||||
| @ -10,6 +10,7 @@ from webauthn import options_to_json | ||||
| from webauthn.helpers.bytes_to_base64url import bytes_to_base64url | ||||
| from webauthn.helpers.exceptions import InvalidRegistrationResponse | ||||
| from webauthn.helpers.structs import ( | ||||
|     AuthenticatorAttachment, | ||||
|     AuthenticatorSelectionCriteria, | ||||
|     PublicKeyCredentialCreationOptions, | ||||
|     ResidentKeyRequirement, | ||||
| @ -91,7 +92,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | ||||
|         # set, cast it to string to ensure it's not a django class | ||||
|         authenticator_attachment = stage.authenticator_attachment | ||||
|         if authenticator_attachment: | ||||
|             authenticator_attachment = str(authenticator_attachment) | ||||
|             authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment)) | ||||
|  | ||||
|         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( | ||||
|             rp_id=get_rp_id(self.request), | ||||
|  | ||||
| @ -30,7 +30,7 @@ class Command(TenantCommand): | ||||
|             delete_stage = True | ||||
|         message = TemplateEmailMessage( | ||||
|             subject="authentik Test-Email", | ||||
|             to=[options["to"]], | ||||
|             to=[("", options["to"])], | ||||
|             template_name="email/setup.html", | ||||
|             template_context={}, | ||||
|         ) | ||||
|  | ||||
| @ -111,7 +111,7 @@ class EmailStageView(ChallengeStageView): | ||||
|         try: | ||||
|             message = TemplateEmailMessage( | ||||
|                 subject=_(current_stage.subject), | ||||
|                 to=[f"{pending_user.name} <{email}>"], | ||||
|                 to=[(pending_user.name, email)], | ||||
|                 language=pending_user.locale(self.request), | ||||
|                 template_name=current_stage.template, | ||||
|                 template_context={ | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| {% load i18n %}{% translate "Welcome!" %} | ||||
| {% load i18n %}{% autoescape off %}{% translate "Welcome!" %} | ||||
|  | ||||
| {% translate "We're excited to have you get started. First, you need to confirm your account. Just open the link below." %} | ||||
|  | ||||
| @ -6,3 +6,4 @@ | ||||
|  | ||||
| --  | ||||
| Powered by goauthentik.io. | ||||
| {% endautoescape %} | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| {% load authentik_stages_email %}{% load i18n %}{% translate "Dear authentik user," %} | ||||
| {% load authentik_stages_email %}{% load i18n %}{% autoescape off %}{% translate "Dear authentik user," %} | ||||
|  | ||||
| {% translate "The following notification was created:" %} | ||||
|  | ||||
| @ -16,3 +16,4 @@ This email was sent from the notification transport {{ name }}. | ||||
|  | ||||
| --  | ||||
| Powered by goauthentik.io. | ||||
| {% endautoescape %} | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| {% load i18n %}{% load humanize %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %} | ||||
| {% load i18n %}{% load humanize %}{% autoescape off %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %} | ||||
|  | ||||
| {% blocktrans %} | ||||
| You recently requested to change your password for your authentik account. Use the link below to set a new password. | ||||
| @ -10,3 +10,4 @@ If you did not request a password change, please ignore this Email. The link abo | ||||
|  | ||||
| --  | ||||
| Powered by goauthentik.io. | ||||
| {% endautoescape %} | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| {% load i18n %}authentik Test-Email | ||||
| {% load i18n %}{% autoescape off %}authentik Test-Email | ||||
| {% blocktrans %} | ||||
| This is a test email to inform you, that you've successfully configured authentik emails. | ||||
| {% endblocktrans %} | ||||
|  | ||||
| --  | ||||
| Powered by goauthentik.io. | ||||
| {% endautoescape %} | ||||
|  | ||||
| @ -39,6 +39,7 @@ class TestEmailStageSending(FlowTestCase): | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         Event.objects.filter(action=EventAction.EMAIL_SENT).delete() | ||||
|  | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||
|         with patch( | ||||
|  | ||||
| @ -9,6 +9,7 @@ from unittest.mock import PropertyMock, patch | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.mail.backends.locmem import EmailBackend | ||||
| from django.core.mail.message import sanitize_address | ||||
| from django.urls import reverse | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| @ -19,6 +20,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||
| from authentik.flows.tests import FlowTestCase | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.stages.email.models import EmailStage, get_template_choices | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
|  | ||||
|  | ||||
| def get_templates_setting(temp_dir: str) -> dict[str, Any]: | ||||
| @ -89,3 +91,12 @@ class TestEmailStageTemplates(FlowTestCase): | ||||
|                     event.context["message"], "Exception occurred while rendering E-mail template" | ||||
|                 ) | ||||
|                 self.assertEqual(event.context["template"], "invalid.html") | ||||
|  | ||||
|     def test_template_address(self): | ||||
|         """Test addresses are correctly parsed""" | ||||
|         message = TemplateEmailMessage(to=[("foo@bar.baz", "foo@bar.baz")]) | ||||
|         [sanitize_address(addr, "utf-8") for addr in message.recipients()] | ||||
|         self.assertEqual(message.recipients(), ["foo@bar.baz"]) | ||||
|         message = TemplateEmailMessage(to=[("some-name", "foo@bar.baz")]) | ||||
|         [sanitize_address(addr, "utf-8") for addr in message.recipients()] | ||||
|         self.assertEqual(message.recipients(), ["some-name <foo@bar.baz>"]) | ||||
|  | ||||
| @ -25,8 +25,19 @@ def logo_data() -> MIMEImage: | ||||
| class TemplateEmailMessage(EmailMultiAlternatives): | ||||
|     """Wrapper around EmailMultiAlternatives with integrated template rendering""" | ||||
|  | ||||
|     def __init__(self, template_name=None, template_context=None, language="", **kwargs): | ||||
|         super().__init__(**kwargs) | ||||
|     def __init__( | ||||
|         self, to: list[tuple[str]], template_name=None, template_context=None, language="", **kwargs | ||||
|     ): | ||||
|         sanitized_to = [] | ||||
|         # Ensure that all recipients are valid | ||||
|         for recipient_name, recipient_email in to: | ||||
|             if recipient_name == recipient_email: | ||||
|                 sanitized_to.append(recipient_email) | ||||
|             else: | ||||
|                 sanitized_to.append(f"{recipient_name} <{recipient_email}>") | ||||
|         super().__init__(to=sanitized_to, **kwargs) | ||||
|         if not template_name: | ||||
|             return | ||||
|         with translation.override(language): | ||||
|             html_content = render_to_string(template_name, template_context) | ||||
|             try: | ||||
|  | ||||
| @ -12,6 +12,7 @@ from rest_framework.exceptions import ValidationError | ||||
| from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER | ||||
| from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes | ||||
| from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION | ||||
| from authentik.events.utils import sanitize_item | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views.executor import FlowExecutorView | ||||
| @ -47,7 +48,7 @@ class UserWriteStageView(StageView): | ||||
|         # this is just a sanity check to ensure that is removed | ||||
|         if parts[0] == "attributes": | ||||
|             parts = parts[1:] | ||||
|         set_path_in_dict(user.attributes, ".".join(parts), value) | ||||
|         set_path_in_dict(user.attributes, ".".join(parts), sanitize_item(value)) | ||||
|  | ||||
|     def ensure_user(self) -> tuple[Optional[User], bool]: | ||||
|         """Ensure a user exists""" | ||||
|  | ||||
| @ -87,11 +87,6 @@ class Tenant(TenantMixin, SerializerModel): | ||||
|             raise IntegrityError("Cannot create schema named template") | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def delete(self, *args, **kwargs): | ||||
|         if self.schema_name in ("public", "template"): | ||||
|             raise IntegrityError("Cannot delete schema public or template") | ||||
|         super().delete(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> Serializer: | ||||
|         from authentik.tenants.api.tenants import TenantSerializer | ||||
|  | ||||
							
								
								
									
										14
									
								
								authentik/tenants/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/tenants/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| """authentik tenants signals""" | ||||
|  | ||||
| from django.db import models | ||||
| from django.db.models.signals import pre_delete | ||||
| from django.dispatch import receiver | ||||
| from django_tenants.utils import get_public_schema_name | ||||
|  | ||||
| from authentik.tenants.models import Tenant | ||||
|  | ||||
|  | ||||
| @receiver(pre_delete, sender=Tenant) | ||||
| def tenants_ensure_no_default_delete(sender, instance: Tenant, **kwargs): | ||||
|     if instance.schema_name == get_public_schema_name(): | ||||
|         raise models.ProtectedError("Cannot delete schema public", instance) | ||||
| @ -32,7 +32,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.4} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -53,7 +53,7 @@ services: | ||||
|       - postgresql | ||||
|       - redis | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.4} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
| @ -50,12 +50,12 @@ type StorageConfig struct { | ||||
| } | ||||
|  | ||||
| type StorageMediaConfig struct { | ||||
| 	Backend string            `yaml:"backend" env:"AUTHENTIK_STORAGE_MEDIA_BACKEND"` | ||||
| 	Backend string            `yaml:"backend" env:"AUTHENTIK_STORAGE__MEDIA__BACKEND"` | ||||
| 	File    StorageFileConfig `yaml:"file"` | ||||
| } | ||||
|  | ||||
| type StorageFileConfig struct { | ||||
| 	Path string `yaml:"path" env:"AUTHENTIK_STORAGE_MEDIA_FILE_PATH"` | ||||
| 	Path string `yaml:"path" env:"AUTHENTIK_STORAGE__MEDIA__FILE__PATH"` | ||||
| } | ||||
|  | ||||
| type ErrorReportingConfig struct { | ||||
|  | ||||
| @ -29,4 +29,4 @@ func UserAgent() string { | ||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2023.10.7" | ||||
| const VERSION = "2024.2.4" | ||||
|  | ||||
| @ -55,6 +55,7 @@ function cleanup { | ||||
| } | ||||
|  | ||||
| function prepare_debug { | ||||
|     source ${VENV_PATH}/bin/activate | ||||
|     poetry install --no-ansi --no-interaction | ||||
|     touch /unittest.xml | ||||
|     chown authentik:authentik /unittest.xml | ||||
|  | ||||
| @ -64,6 +64,7 @@ def release_lock(cursor: Cursor): | ||||
|     """Release database lock""" | ||||
|     if not LOCKED: | ||||
|         return | ||||
|     LOGGER.info("releasing database lock") | ||||
|     cursor.execute("SELECT pg_advisory_unlock(%s)", (ADV_LOCK_UID,)) | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										12
									
								
								lifecycle/system_migrations/template_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								lifecycle/system_migrations/template_schema.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| from lifecycle.migrate import BaseMigration | ||||
|  | ||||
|  | ||||
| class Migration(BaseMigration): | ||||
|     def needs_migration(self) -> bool: | ||||
|         self.cur.execute( | ||||
|             "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'template';" | ||||
|         ) | ||||
|         return not bool(self.cur.rowcount) | ||||
|  | ||||
|     def run(self): | ||||
|         self.cur.execute("CREATE SCHEMA IF NOT EXISTS template; COMMIT;") | ||||
							
								
								
									
										148
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										148
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -506,48 +506,48 @@ files = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "cbor2" | ||||
| version = "5.5.1" | ||||
| version = "5.6.2" | ||||
| description = "CBOR (de)serializer with extensive tag support" | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:37ba4f719384bd4ea317e92a8763ea343e205f3112c8241778fd9dbc64ae1498"}, | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:425ae919120b9d05b4794b3e5faf6584fc47a9d61db059d4f00ce16ae93a3f63"}, | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c511ff6356d6f4292ced856d5048a24ee61a85634816f29dadf1f089e8cb4f9"}, | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6ab54a9282dd99a3a70d0f64706d3b3592e7920564a93101caa74dec322346c"}, | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:39d94852dd61bda5b3d2bfe74e7b194a7199937d270f90099beec3e7584f0c9b"}, | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65532ba929beebe1c63317ad00c79d4936b60a5c29a3c329d2aa7df4e72ad907"}, | ||||
|     {file = "cbor2-5.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1206180f66a9ad23e692cf457610c877f186ad303a1264b6c5335015b7bee83e"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:42155a20be46312fad2ceb85a408e2d90da059c2d36a65e0b99abca57c5357fd"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f3827ae14c009df9b37790f1da5cd1f9d64f7ffec472a49ebf865c0af6b77e9"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bfa417dbb8b4581ad3c2312469899518596551cfb0fe5bdaf8a6921cff69d7e"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3317e7dfb4f3180be90bcd853204558d89f119b624c2168153b53dea305e79d"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a5770bdf4340de55679efe6c38fc6d64529fda547e7a85eb0217a82717a8235"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b5d53826ad0c92fcb004b2a475896610b51e0ca010f6c37d762aae44ab0807b2"}, | ||||
|     {file = "cbor2-5.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc77cac985f7f7a20f2d8b1957d1e79393d7df823f61c7c6173d3a0011c1d770"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9e45d5aa8e484b4bf57240d8e7949389f1c9d4073758abb30954386321b55c9d"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93b949a66bec40dd0ca87a6d026136fea2cf1660120f921199a47ac8027af253"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d601ca92d917f769370a5e6c3ead62dca6451b2b603915e4fcf300083b9fcd"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11876abd50b9f70d114fcdbb0b5a3249ccd7d321465f0350028fd6d2317e114"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fd77c558decdba2a2a7a463e6346d53781d2163bacf205f77b999f561ba4ac73"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efb81920d80410b8e80a4a6a8b06ec9b766be0ae7f3029af8ae4b30914edcfa3"}, | ||||
|     {file = "cbor2-5.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:4bb35f3b1ebd4b7b37628f0cd5c839f3008dec669194a2a4a33d91bab7f8663b"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f41e4a439f642954ed728dc18915098b5f2ebec7029eaebe52c06c52b6a9a63a"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4eae4d56314f22920a28bf7affefdfc918646877ce3b16220dc6cf38a584aa41"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559a0c1ec8dcedd6142b81727403e0f5a2e8f4c18e8bb3c548107ec39af4e9cb"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537da7bfee97ee44a11b300c034c18e674af6a5dc4718a6fba141037f099c7ec"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c99fd8bbc6bbf3bf4d6b2996594ae633b778b27b0531559487950762c4e1e3f"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ee46e6dbc8e2cf302a022fec513d57dba65e9d5ec495bcd1ad97a5dbdbab249"}, | ||||
|     {file = "cbor2-5.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:67e2be461320197495fff55f250b111d4125a0a2d02e6256e41f8598adc3ad3f"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4384a56afef0b908b61c8ea3cca3e257a316427ace3411308f51ee301b23adf9"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8cc64acc606b7f2a4b673a1d6cde5a9cb1860a6ce27b353e269c9535efbd62c"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50019fea3cb07fa9b2b53772a52b4243e87de232591570c4c272b3ebdb419493"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a18be0af9241883bc67a036c1f33e3f9956d31337ccd412194bf759bc1095e03"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:60e7e0073291096605de27de3ce006148cf9a095199160439555f14f93d044d5"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41f7501338228b27dac88c1197928cf8985f6fc775f59be89c6fdaddb4e69658"}, | ||||
|     {file = "cbor2-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c85ab7697252af2240e939707c935ea18081ccb580d4b5b9a94b04148ab2c32b"}, | ||||
|     {file = "cbor2-5.5.1-py3-none-any.whl", hash = "sha256:dca639c8ff81b9f0c92faf97324adfdbfb5c2a5bb97f249606c6f5b94c77cc0d"}, | ||||
|     {file = "cbor2-5.5.1.tar.gz", hash = "sha256:f9e192f461a9f8f6082df28c035b006d153904213dc8640bed8a72d72bbc9475"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:516b8390936bb172ff18d7b609a452eaa51991513628949b0a9bf25cbe5a7129"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b8b504b590367a51fe8c0d9b8cb458a614d782d37b24483097e2b1e93ed0fff"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f687e6731b1198811223576800258a712ddbfdcfa86c0aee2cc8269193e6b96"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e94043d99fe779f62a15a5e156768588a2a7047bb3a127fa312ac1135ff5ecb"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8af7162fcf7aa2649f02563bdb18b2fa6478b751eee4df0257bffe19ea8f107a"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ea7ecd81c5c6e02c2635973f52a0dd1e19c0bf5ef51f813d8cd5e3e7ed072726"}, | ||||
|     {file = "cbor2-5.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3c7f223f1fedc74d33f363d184cb2bab9e4bdf24998f73b5e3bef366d6c41628"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ea9e150029c3976c46ee9870b6dcdb0a5baae21008fe3290564886b11aa2b64"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:922e06710e5cf6f56b82b0b90d2f356aa229b99e570994534206985f675fd307"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b01a718e083e6de8b43296c3ccdb3aa8af6641f6bbb3ea1700427c6af73db28a"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac85eb731c524d148f608b9bdb2069fa79e374a10ed5d10a2405eba9a6561e60"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03e5b68867b9d89ff2abd14ef7c6d42fbd991adc3e734a19a294935f22a4d05a"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7221b83000ee01d674572eec1d1caa366eac109d1d32c14d7af9a4aaaf496563"}, | ||||
|     {file = "cbor2-5.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9aca73b63bdc6561e1a0d38618e78b9c204c942260d51e663c92c4ba6c961684"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:377cfe9d5560c682486faef6d856226abf8b2801d95fa29d4e5d75b1615eb091"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fdc564ef2e9228bcd96ec8c6cdaa431a48ab03b3fb8326ead4b3f986330e5b9e"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d1c0021d9a1f673066de7c8941f71a59abb11909cc355892dda01e79a2b3045"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fde9e704e96751e0729cc58b912d0e77c34387fb6bcceea0817069e8683df45"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:30e9ba8f4896726ca61869efacda50b6859aff92162ae5a0e192859664f36c81"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a1e18e65ac71e04434ff5b58bde5c53f85b9c5bc92a3c0e2265089d3034f3"}, | ||||
|     {file = "cbor2-5.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:94981277b4bf448a2754c1f34a9d0055a9d1c5a8d102c933ffe95c80f1085bae"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f70db0ebcf005c25408e8d5cc4b9558c899f13a3e2f8281fa3d3be4894e0e821"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:22c24fe9ef1696a84b8fd80ff66eb0e5234505d8b9a9711fc6db57bce10771f3"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4a3420f80d6b942874d66eaad07658066370df994ddee4125b48b2cbc61ece"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b28d8ff0e726224a7429281700c28afe0e665f83f9ae79648cbae3f1a391cbf"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c10ede9462458998f1b9c488e25fe3763aa2491119b7af472b72bf538d789e24"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea686dfb5e54d690e704ce04993bc8ca0052a7cd2d4b13dd333a41cca8a05a05"}, | ||||
|     {file = "cbor2-5.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:22996159b491d545ecfd489392d3c71e5d0afb9a202dfc0edc8b2cf413a58326"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9faa0712d414a88cc1244c78cd4b28fced44f1827dbd8c1649e3c40588aa670f"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6031a284d93fc953fc2a2918f261c4f5100905bd064ca3b46961643e7312a828"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30c8a9a9df79f26e72d8d5fa51ef08eb250d9869a711bcf9539f1865916c983"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bf7457fca23209e14dab8181dff82466a83b72e55b444dbbfe90fa67659492"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc29c068687aa2e7778f63b653f1346065b858427a2555df4dc2191f4a0de8ce"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42eaf0f768bd27afcb38135d5bfc361d3a157f1f5c7dddcd8d391f7fa43d9de8"}, | ||||
|     {file = "cbor2-5.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:8839b73befa010358477736680657b9d08c1ed935fd973decb1909712a41afdc"}, | ||||
|     {file = "cbor2-5.6.2-py3-none-any.whl", hash = "sha256:c0b53a65673550fde483724ff683753f49462d392d45d7b6576364b39e76e54c"}, | ||||
|     {file = "cbor2-5.6.2.tar.gz", hash = "sha256:b7513c2dea8868991fad7ef8899890ebcf8b199b9b4461c3c11d7ad3aef4820d"}, | ||||
| ] | ||||
|  | ||||
| [package.extras] | ||||
| @ -993,43 +993,43 @@ toml = ["tomli"] | ||||
|  | ||||
| [[package]] | ||||
| name = "cryptography" | ||||
| version = "42.0.2" | ||||
| version = "42.0.4" | ||||
| description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, | ||||
|     {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, | ||||
|     {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, | ||||
|     {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, | ||||
|     {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, | ||||
|     {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, | ||||
|     {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, | ||||
|     {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, | ||||
|     {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, | ||||
|     {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, | ||||
|     {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, | ||||
|     {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"}, | ||||
|     {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"}, | ||||
|     {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"}, | ||||
|     {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"}, | ||||
|     {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"}, | ||||
|     {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"}, | ||||
|     {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"}, | ||||
|     {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"}, | ||||
|     {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"}, | ||||
|     {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"}, | ||||
|     {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"}, | ||||
|     {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @ -3934,13 +3934,13 @@ wsproto = ">=0.14" | ||||
|  | ||||
| [[package]] | ||||
| name = "twilio" | ||||
| version = "8.13.0" | ||||
| version = "8.12.0" | ||||
| description = "Twilio API client and TwiML generator" | ||||
| optional = false | ||||
| python-versions = ">=3.7.0" | ||||
| files = [ | ||||
|     {file = "twilio-8.13.0-py2.py3-none-any.whl", hash = "sha256:f5396e355de11b80c6729bd286fdc0e12c9c0b025c465f16f090034a7ef88d3d"}, | ||||
|     {file = "twilio-8.13.0.tar.gz", hash = "sha256:89f629fa280b51bc21cd58b35cf640f9bbf88efd3977c0c5ec6ea6821b9880cd"}, | ||||
|     {file = "twilio-8.12.0-py2.py3-none-any.whl", hash = "sha256:ccdf78b634dff13fd50b33bafd254a9dc2fb492c6b06839683e73f09ec110ec6"}, | ||||
|     {file = "twilio-8.12.0.tar.gz", hash = "sha256:28e3a743da18d5b298c9b9fb9e922404a60f8091441c5e0aa35bfefc46411370"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
|  | ||||
| @ -113,7 +113,7 @@ filterwarnings = [ | ||||
|  | ||||
| [tool.poetry] | ||||
| name = "authentik" | ||||
| version = "2023.10.7" | ||||
| version = "2024.2.4" | ||||
| description = "" | ||||
| authors = ["authentik Team <hello@goauthentik.io>"] | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2023.10.7 | ||||
|   version: 2024.2.4 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     email: hello@goauthentik.io | ||||
|  | ||||
							
								
								
									
										289
									
								
								tests/wdio/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										289
									
								
								tests/wdio/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -6,16 +6,16 @@ | ||||
|         "": { | ||||
|             "name": "@goauthentik/web-tests", | ||||
|             "dependencies": { | ||||
|                 "chromedriver": "^121.0.2" | ||||
|                 "chromedriver": "^121.0.0" | ||||
|             }, | ||||
|             "devDependencies": { | ||||
|                 "@trivago/prettier-plugin-sort-imports": "^4.3.0", | ||||
|                 "@typescript-eslint/eslint-plugin": "^7.0.1", | ||||
|                 "@typescript-eslint/parser": "^7.0.1", | ||||
|                 "@wdio/cli": "^8.32.2", | ||||
|                 "@wdio/local-runner": "^8.32.2", | ||||
|                 "@wdio/mocha-framework": "^8.32.2", | ||||
|                 "@wdio/spec-reporter": "^8.32.2", | ||||
|                 "@wdio/cli": "^8.31.1", | ||||
|                 "@wdio/local-runner": "^8.31.1", | ||||
|                 "@wdio/mocha-framework": "^8.31.1", | ||||
|                 "@wdio/spec-reporter": "^8.31.1", | ||||
|                 "eslint": "^8.56.0", | ||||
|                 "eslint-config-google": "^0.14.0", | ||||
|                 "eslint-plugin-sonarjs": "^0.24.0", | ||||
| @ -896,9 +896,9 @@ | ||||
|             "devOptional": true | ||||
|         }, | ||||
|         "node_modules/@types/normalize-package-data": { | ||||
|             "version": "2.4.4", | ||||
|             "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", | ||||
|             "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", | ||||
|             "version": "2.4.2", | ||||
|             "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", | ||||
|             "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/@types/semver": { | ||||
| @ -1187,19 +1187,19 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/cli": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.32.2.tgz", | ||||
|             "integrity": "sha512-CbTALXXnDzvthu+EK0dK5QDTXToU4wNrldtonQcsD8tT726O56BLFGm9tzcGP6PZWh9NxAuvsFlWSwUxcqXq0Q==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.31.1.tgz", | ||||
|             "integrity": "sha512-UnAoXjUrgRTfFq7TSnnMSuA80V8G7yW/d5zo59RtzrHdrGr6QVWJfnt6aLueXJJ6SnouVA6yU2rcsXPOvGpUIA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.1", | ||||
|                 "@vitest/snapshot": "^1.2.1", | ||||
|                 "@wdio/config": "8.32.2", | ||||
|                 "@wdio/globals": "8.32.2", | ||||
|                 "@wdio/config": "8.31.1", | ||||
|                 "@wdio/globals": "8.31.1", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/protocols": "8.32.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/utils": "8.32.2", | ||||
|                 "@wdio/protocols": "8.29.7", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "@wdio/utils": "8.31.1", | ||||
|                 "async-exit-hook": "^2.0.1", | ||||
|                 "chalk": "^5.2.0", | ||||
|                 "chokidar": "^3.5.3", | ||||
| @ -1208,13 +1208,13 @@ | ||||
|                 "ejs": "^3.1.9", | ||||
|                 "execa": "^8.0.1", | ||||
|                 "import-meta-resolve": "^4.0.0", | ||||
|                 "inquirer": "9.2.12", | ||||
|                 "inquirer": "9.2.14", | ||||
|                 "lodash.flattendeep": "^4.4.0", | ||||
|                 "lodash.pickby": "^4.6.0", | ||||
|                 "lodash.union": "^4.6.0", | ||||
|                 "read-pkg-up": "10.0.0", | ||||
|                 "read-pkg-up": "^10.0.0", | ||||
|                 "recursive-readdir": "^2.2.3", | ||||
|                 "webdriverio": "8.32.2", | ||||
|                 "webdriverio": "8.31.1", | ||||
|                 "yargs": "^17.7.2" | ||||
|             }, | ||||
|             "bin": { | ||||
| @ -1237,14 +1237,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/config": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.32.2.tgz", | ||||
|             "integrity": "sha512-ubqe4X+TgcERzXKIpMfisquNxPZNtRU5uPeV7hvas++mD75QyNpmWHCtea2+TjoXKxlZd1MVrtZAwtmqMmyhPw==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.31.1.tgz", | ||||
|             "integrity": "sha512-Iz4DTXQdy53VT8LRZ6ayaDKE+zEDk4QY/ILz+D0IQh0OaMWruFesfoxqFP0hnU6rbJT1YE4ehTGf7JTZLWIPcw==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/utils": "8.32.2", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "@wdio/utils": "8.31.1", | ||||
|                 "decamelize": "^6.0.0", | ||||
|                 "deepmerge-ts": "^5.0.0", | ||||
|                 "glob": "^10.2.2", | ||||
| @ -1255,29 +1255,29 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/globals": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.32.2.tgz", | ||||
|             "integrity": "sha512-WEN7Tq0+Ny8OHS+7e1RCu4ss3lYG2Ln8/TpicacTsYWM3jtrf1SkUT+4H8JtG1qywj4WXTH8CxD4H9zFoofiJw==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.31.1.tgz", | ||||
|             "integrity": "sha512-2r80BX8aS5YiQ6cFuxtt44g2Y5P01MoHJTR3w21suSyiVoH70mxvJf6vJsLB+jtGfaXLJzOjlZkgXrd+Kn+keA==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": "^16.13 || >=18" | ||||
|             }, | ||||
|             "optionalDependencies": { | ||||
|                 "expect-webdriverio": "^4.11.2", | ||||
|                 "webdriverio": "8.32.2" | ||||
|                 "webdriverio": "8.31.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/local-runner": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.32.2.tgz", | ||||
|             "integrity": "sha512-iCVnwBIIwgRqiF2Fz/XVlGf3ejHOzR2+kc3dsdMgKljLkVq4ZDFwPMtw8Hk06sq+BaiNLamdxj+8ElD6OGJ88A==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.31.1.tgz", | ||||
|             "integrity": "sha512-KaMok/LaVvWcXTTi61Al5+XgocBDJ2+gpG1mjxytILrwaMFohYL0YuaGDw4yb3lx3Lsej6K+/FbaWMMnGq3JIw==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.0", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/repl": "8.24.12", | ||||
|                 "@wdio/runner": "8.32.2", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/runner": "8.31.1", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "async-exit-hook": "^2.0.1", | ||||
|                 "split2": "^4.1.0", | ||||
|                 "stream-buffers": "^3.0.2" | ||||
| @ -1314,16 +1314,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/mocha-framework": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.32.2.tgz", | ||||
|             "integrity": "sha512-76DA95u0Z2AfyZSQglJn9BFJ+XveRR6+oH1K/J8rDOWoIHgMcASGEj+fsXAPSDNcSVDBe0QN5HLnVi3tciuyQw==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.31.1.tgz", | ||||
|             "integrity": "sha512-5297tKj9zNvzZD+X4tMSuTcJrSaQ6mmDGLEdapa/+CMA549N+0vE38tNh+5Br7dmFXXVanw40T752OQcSeFf1A==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/mocha": "^10.0.0", | ||||
|                 "@types/node": "^20.1.0", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/utils": "8.32.2", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "@wdio/utils": "8.31.1", | ||||
|                 "mocha": "^10.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -1331,9 +1331,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/protocols": { | ||||
|             "version": "8.32.0", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.32.0.tgz", | ||||
|             "integrity": "sha512-inLJRrtIGdTz/YPbcsvpSvPlYQFTVtF3OYBwAXhG2FiP1ZwE1CQNLP/xgRGye1ymdGCypGkexRqIx3KBGm801Q==", | ||||
|             "version": "8.29.7", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.29.7.tgz", | ||||
|             "integrity": "sha512-9hhEePMLmI8fm9F2v4jlg9x4w4jEoZmY3vT6fXy90ne1DFaGWfy/a853nKEagQe/ZzxkN3/cpMBh8mryv9BVjw==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/@wdio/repl": { | ||||
| @ -1349,14 +1349,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/reporter": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.32.2.tgz", | ||||
|             "integrity": "sha512-BZdReAFfRCtgtYbyhkKgSGqqoIn/yG5/Z4COjvRvon9NJkz+eA4PiHCKdEP7+ekBIjud7H8Gy+6mPBDEu+wllw==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.31.1.tgz", | ||||
|             "integrity": "sha512-ayZipzyr9dSwpbKYbV4PoXuw91A1H7fjadJ5R5oMYUETx+pfBJqjR2UIHZhhPAa0lJ7bWHEn7eCsGB1PXp47Og==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.0", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "diff": "^5.0.0", | ||||
|                 "object-inspect": "^1.12.0" | ||||
|             }, | ||||
| @ -1365,35 +1365,35 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/runner": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.32.2.tgz", | ||||
|             "integrity": "sha512-/24wPBg2okkfpA1bl7mCWIvWXO3pa9OhsKNav+gGm7BP+hQ1lxULyYg/o5fCEwEjFPWDLy0jjknJLqXcTWvmiQ==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.31.1.tgz", | ||||
|             "integrity": "sha512-9KUDaAHNUeBp5h1YoEmKVk0hZyzBDl6x0ge1dnaORACKKi6TXa76T0kLaBruuBTBn4zZd4+Xrg9/bLGAnTFvLA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.0", | ||||
|                 "@wdio/config": "8.32.2", | ||||
|                 "@wdio/globals": "8.32.2", | ||||
|                 "@wdio/config": "8.31.1", | ||||
|                 "@wdio/globals": "8.31.1", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/utils": "8.32.2", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "@wdio/utils": "8.31.1", | ||||
|                 "deepmerge-ts": "^5.0.0", | ||||
|                 "expect-webdriverio": "^4.11.2", | ||||
|                 "gaze": "^1.1.2", | ||||
|                 "webdriver": "8.32.2", | ||||
|                 "webdriverio": "8.32.2" | ||||
|                 "webdriver": "8.31.1", | ||||
|                 "webdriverio": "8.31.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^16.13 || >=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/spec-reporter": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.32.2.tgz", | ||||
|             "integrity": "sha512-3hUXpE+idC4KOXofJnpubdDDCE8X0OTd6ykypiaXMI2hJTA2nIZcHtpRQnAE0E4JT9OzLnPWODcMq54GGBDRkg==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.31.1.tgz", | ||||
|             "integrity": "sha512-t2isqf/yDvc3xfNnkuR8XdRaKf34I6/40f1DHfojHZUpTF94jrt7CLACkFiDZhu5sz0KCuDWzy56aycd6IUz3w==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@wdio/reporter": "8.32.2", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/reporter": "8.31.1", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "chalk": "^5.1.2", | ||||
|                 "easy-table": "^1.2.0", | ||||
|                 "pretty-ms": "^7.0.0" | ||||
| @ -1415,9 +1415,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/types": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.32.2.tgz", | ||||
|             "integrity": "sha512-jq8LcBBQpBP9ZF5kECKEpXv8hN7irCGCjLFAN0Bd5ScRR6qu6MGWvwkDkau2sFPr0b++sKDCEaMzQlwrGFjZXg==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.31.1.tgz", | ||||
|             "integrity": "sha512-KQ0EmjeVdshufhsxygaPzkJ8WD7hm8WlflZcLwKMZ0OM6f8pV9NMGGOvfBQXgTs447ScK6/6rX+lbJk3yvg65g==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.0" | ||||
| @ -1427,14 +1427,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@wdio/utils": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.32.2.tgz", | ||||
|             "integrity": "sha512-PJcP4d1Fr8Zp+YIfGN93G0fjDj/6J0I6Gf6p0IpJk8qKQpdFDm4gB+lc202iv2YkyC+oT6b4Ik2W9LzvpSKNoQ==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.31.1.tgz", | ||||
|             "integrity": "sha512-fGUtNeJYSqPLMqIRrooEg1ViM2+z1Izd/7bzWzhg8EQHKFXqD/G68rEwBWpoLF/ziiHZFe4fJk7SZdXUK/gFgQ==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@puppeteer/browsers": "^1.6.0", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "decamelize": "^6.0.0", | ||||
|                 "deepmerge-ts": "^5.1.0", | ||||
|                 "edgedriver": "^5.3.5", | ||||
| @ -2064,9 +2064,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/chromedriver": { | ||||
|             "version": "121.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.2.tgz", | ||||
|             "integrity": "sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==", | ||||
|             "version": "121.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.0.tgz", | ||||
|             "integrity": "sha512-ZIKEdZrQAfuzT/RRofjl8/EZR99ghbdBXNTOcgJMKGP6N/UL6lHUX4n6ONWBV18pDvDFfQJ0x58h5AdOaXIOMw==", | ||||
|             "hasInstallScript": true, | ||||
|             "dependencies": { | ||||
|                 "@testim/chrome-version": "^1.1.4", | ||||
| @ -2589,15 +2589,15 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/devtools-protocol": { | ||||
|             "version": "0.0.1261483", | ||||
|             "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1261483.tgz", | ||||
|             "integrity": "sha512-7vJvejpzA5DTfZVkr7a8sGpEAzEiAqcgmRTB0LSUrWeOicwL09lMQTzxHtFNVhJ1OOJkgYdH6Txvy9E5j3VOUQ==", | ||||
|             "version": "0.0.1255431", | ||||
|             "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1255431.tgz", | ||||
|             "integrity": "sha512-VuKgO1U4Ew4meKKoXCEBMUNkzyQqci5F8HIuoELPJkr5yvk9kR9p07gaZfzG9QIIrcIfpJVgf6Ms8OqEMxEYgA==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/diff": { | ||||
|             "version": "5.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", | ||||
|             "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", | ||||
|             "version": "5.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", | ||||
|             "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=0.3.1" | ||||
| @ -3445,40 +3445,15 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/figures": { | ||||
|             "version": "5.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", | ||||
|             "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", | ||||
|             "version": "3.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", | ||||
|             "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "escape-string-regexp": "^5.0.0", | ||||
|                 "is-unicode-supported": "^1.2.0" | ||||
|                 "escape-string-regexp": "^1.0.5" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=14" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/figures/node_modules/escape-string-regexp": { | ||||
|             "version": "5.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", | ||||
|             "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/figures/node_modules/is-unicode-supported": { | ||||
|             "version": "1.3.0", | ||||
|             "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", | ||||
|             "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=12" | ||||
|                 "node": ">=8" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
| @ -4387,18 +4362,18 @@ | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/inquirer": { | ||||
|             "version": "9.2.12", | ||||
|             "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", | ||||
|             "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", | ||||
|             "version": "9.2.14", | ||||
|             "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz", | ||||
|             "integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@ljharb/through": "^2.3.11", | ||||
|                 "@ljharb/through": "^2.3.12", | ||||
|                 "ansi-escapes": "^4.3.2", | ||||
|                 "chalk": "^5.3.0", | ||||
|                 "cli-cursor": "^3.1.0", | ||||
|                 "cli-width": "^4.1.0", | ||||
|                 "external-editor": "^3.1.0", | ||||
|                 "figures": "^5.0.0", | ||||
|                 "figures": "^3.2.0", | ||||
|                 "lodash": "^4.17.21", | ||||
|                 "mute-stream": "1.0.0", | ||||
|                 "ora": "^5.4.1", | ||||
| @ -4409,7 +4384,7 @@ | ||||
|                 "wrap-ansi": "^6.2.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=14.18.0" | ||||
|                 "node": ">=18" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/inquirer/node_modules/chalk": { | ||||
| @ -5313,9 +5288,9 @@ | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/json-parse-even-better-errors": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", | ||||
|             "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", | ||||
|             "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": "^14.17.0 || ^16.13.0 || >=18.0.0" | ||||
| @ -5425,9 +5400,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/lines-and-columns": { | ||||
|             "version": "2.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", | ||||
|             "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", | ||||
|             "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | ||||
| @ -7052,14 +7027,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up": { | ||||
|             "version": "10.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.0.0.tgz", | ||||
|             "integrity": "sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==", | ||||
|             "version": "10.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", | ||||
|             "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "find-up": "^6.3.0", | ||||
|                 "read-pkg": "^8.0.0", | ||||
|                 "type-fest": "^3.12.0" | ||||
|                 "read-pkg": "^8.1.0", | ||||
|                 "type-fest": "^4.2.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=16" | ||||
| @ -7157,9 +7132,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up/node_modules/parse-json": { | ||||
|             "version": "7.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", | ||||
|             "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", | ||||
|             "version": "7.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.0.tgz", | ||||
|             "integrity": "sha512-ihtdrgbqdONYD156Ap6qTcaGcGdkdAxodO1wLqQ/j7HP1u2sFYppINiq4jyC8F+Nm+4fVufylCV00QmkTHkSUg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@babel/code-frame": "^7.21.4", | ||||
| @ -7175,6 +7150,18 @@ | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up/node_modules/parse-json/node_modules/type-fest": { | ||||
|             "version": "3.13.1", | ||||
|             "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", | ||||
|             "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=14.16" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up/node_modules/path-exists": { | ||||
|             "version": "5.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", | ||||
| @ -7202,10 +7189,10 @@ | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": { | ||||
|             "version": "4.10.2", | ||||
|             "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz", | ||||
|             "integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==", | ||||
|         "node_modules/read-pkg-up/node_modules/type-fest": { | ||||
|             "version": "4.3.1", | ||||
|             "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.3.1.tgz", | ||||
|             "integrity": "sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=16" | ||||
| @ -7214,18 +7201,6 @@ | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up/node_modules/type-fest": { | ||||
|             "version": "3.13.1", | ||||
|             "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", | ||||
|             "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=14.16" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/read-pkg-up/node_modules/yocto-queue": { | ||||
|             "version": "1.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", | ||||
| @ -8773,18 +8748,18 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/webdriver": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.32.2.tgz", | ||||
|             "integrity": "sha512-uyCT2QzCqoz+EsMLTApG5/+RvHJR9MVbdEnjMoxpJDt+IeZCG2Vy/Gq9oNgNQfpxrvZme/EY+PtBsltZi7BAyg==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.31.1.tgz", | ||||
|             "integrity": "sha512-J1Ata+ZiBVhCFKL7hnD6qCfr7ZRsBN2c/YlCgosq0lG/iYMKXWi5rlWDfpuyISprM/G/V3GjfEGxTUC6jJBSBA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.0", | ||||
|                 "@types/ws": "^8.5.3", | ||||
|                 "@wdio/config": "8.32.2", | ||||
|                 "@wdio/config": "8.31.1", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/protocols": "8.32.0", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/utils": "8.32.2", | ||||
|                 "@wdio/protocols": "8.29.7", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "@wdio/utils": "8.31.1", | ||||
|                 "deepmerge-ts": "^5.1.0", | ||||
|                 "got": "^12.6.1", | ||||
|                 "ky": "^0.33.0", | ||||
| @ -8795,23 +8770,23 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/webdriverio": { | ||||
|             "version": "8.32.2", | ||||
|             "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.32.2.tgz", | ||||
|             "integrity": "sha512-Z0Wc/dHFfWGWJZpaQ8u910/LG0E9EIVTO7J5yjqWx2XtXz2LzQMxYwNRnvNLhY/1tI4y/cZxI6kFMWr8wD2TtA==", | ||||
|             "version": "8.31.1", | ||||
|             "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.31.1.tgz", | ||||
|             "integrity": "sha512-b3bLBkkSGESGcRw3s3Sty84luZe2+qwPudXosSXbzcRu2Z1sccjdA6BHJA36IcLgKndNCOhf9wx3yQ3umoS7Jw==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@types/node": "^20.1.0", | ||||
|                 "@wdio/config": "8.32.2", | ||||
|                 "@wdio/config": "8.31.1", | ||||
|                 "@wdio/logger": "8.28.0", | ||||
|                 "@wdio/protocols": "8.32.0", | ||||
|                 "@wdio/protocols": "8.29.7", | ||||
|                 "@wdio/repl": "8.24.12", | ||||
|                 "@wdio/types": "8.32.2", | ||||
|                 "@wdio/utils": "8.32.2", | ||||
|                 "@wdio/types": "8.31.1", | ||||
|                 "@wdio/utils": "8.31.1", | ||||
|                 "archiver": "^6.0.0", | ||||
|                 "aria-query": "^5.0.0", | ||||
|                 "css-shorthand-properties": "^1.1.1", | ||||
|                 "css-value": "^0.0.1", | ||||
|                 "devtools-protocol": "^0.0.1261483", | ||||
|                 "devtools-protocol": "^0.0.1255431", | ||||
|                 "grapheme-splitter": "^1.0.2", | ||||
|                 "import-meta-resolve": "^4.0.0", | ||||
|                 "is-plain-obj": "^4.1.0", | ||||
| @ -8823,7 +8798,7 @@ | ||||
|                 "resq": "^1.9.1", | ||||
|                 "rgb2hex": "0.2.5", | ||||
|                 "serialize-error": "^11.0.1", | ||||
|                 "webdriver": "8.32.2" | ||||
|                 "webdriver": "8.31.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^16.13 || >=18" | ||||
|  | ||||
| @ -6,10 +6,10 @@ | ||||
|         "@trivago/prettier-plugin-sort-imports": "^4.3.0", | ||||
|         "@typescript-eslint/eslint-plugin": "^7.0.1", | ||||
|         "@typescript-eslint/parser": "^7.0.1", | ||||
|         "@wdio/cli": "^8.32.2", | ||||
|         "@wdio/local-runner": "^8.32.2", | ||||
|         "@wdio/mocha-framework": "^8.32.2", | ||||
|         "@wdio/spec-reporter": "^8.32.2", | ||||
|         "@wdio/cli": "^8.31.1", | ||||
|         "@wdio/local-runner": "^8.31.1", | ||||
|         "@wdio/mocha-framework": "^8.31.1", | ||||
|         "@wdio/spec-reporter": "^8.31.1", | ||||
|         "eslint": "^8.56.0", | ||||
|         "eslint-config-google": "^0.14.0", | ||||
|         "eslint-plugin-sonarjs": "^0.24.0", | ||||
| @ -32,6 +32,6 @@ | ||||
|         "node": ">=20" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "chromedriver": "^121.0.2" | ||||
|         "chromedriver": "^121.0.0" | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,33 +0,0 @@ | ||||
| module.exports = { | ||||
|     // Basic syntax: just run these tasks all the packages. There is only one package at the moment. | ||||
|     pipeline: { | ||||
|         "extract-locales": ["^extract-locales"], | ||||
|         "build-locales": ["^build-locales"], | ||||
|         "build-locales:build": ["^build-locales:build"], | ||||
|         "build-locales:repair": ["^build-locales:repair"], | ||||
|         "rollup:build": ["^rollup:build"], | ||||
|         "rollup:build-proxy": ["^rollup:build-proxy"], | ||||
|         "rollup:watch": ["^rollup:watch"], | ||||
|         "build": ["^build"], | ||||
|         "build-proxy": ["^build-proxy"], | ||||
|         "watch": ["^watch"], | ||||
|         "lint": ["^lint"], | ||||
|         "lint:precommit": ["^lint:precommit"], | ||||
|         "lint:spelling": ["^lint:spelling"], | ||||
|         "lit-analyse": ["^lit-analyse"], | ||||
|         "precommit": ["^precommit"], | ||||
|         "prequick": ["^prequick"], | ||||
|         "prettier-check": ["^prettier-check"], | ||||
|         "prettier": ["^prettier"], | ||||
|         "pseudolocalize:build-extract-script": ["^pseudolocalize:build-extract-script"], | ||||
|         "pseudolocalize:extract": ["^pseudolocalize:extract"], | ||||
|         "pseudolocalize": ["^pseudolocalize"], | ||||
|         "tsc:execute": ["^tsc:execute"], | ||||
|         "tsc": ["^tsc"], | ||||
|         "storybook": ["^storybook"], | ||||
|         "storybook:build": ["^storybook:build"], | ||||
|         "storybook:build-import-map": ["^storybook:build-import-map"], | ||||
|         "storybook:build-import-map-script": ["^storybook:build-import-map-script"], | ||||
|         "storybook:run-import-map-script": ["^storybook:run-import-map-script"], | ||||
|     }, | ||||
| }; | ||||
							
								
								
									
										1887
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1887
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										159
									
								
								web/package.json
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								web/package.json
									
									
									
									
									
								
							| @ -4,38 +4,132 @@ | ||||
|     "private": true, | ||||
|     "license": "MIT", | ||||
|     "scripts": { | ||||
|         "extract-locales": "lage extract-locales", | ||||
|         "build-locales": "lage build-locales", | ||||
|         "build-locales:build": "lage build-locales:build", | ||||
|         "build-locales:repair": "lage build-locales:repair", | ||||
|         "rollup:build": "lage rollup:build", | ||||
|         "rollup:build-proxy": "lage rollup:build-proxy", | ||||
|         "rollup:watch": "lage rollup:watch", | ||||
|         "build": "lage build", | ||||
|         "build-proxy": "lage build-proxy", | ||||
|         "watch": "lage watch", | ||||
|         "lint": "lage lint", | ||||
|         "lint:precommit": "lage lint:precommit", | ||||
|         "lint:spelling": "lage lint:spelling", | ||||
|         "lit-analyse": "lage lit-analyse", | ||||
|         "precommit": "lage precommit", | ||||
|         "prequick": "lage prequick", | ||||
|         "prettier-check": "lage prettier-check", | ||||
|         "prettier": "lage prettier", | ||||
|         "pseudolocalize:build-extract-script": "lage pseudolocalize:build-extract-script", | ||||
|         "pseudolocalize:extract": "lage pseudolocalize:extract", | ||||
|         "pseudolocalize": "lage pseudolocalize", | ||||
|         "tsc:execute": "lage tsc:execute", | ||||
|         "tsc": "lage tsc", | ||||
|         "storybook": "lage storybook", | ||||
|         "storybook:build": "lage storybook:build", | ||||
|         "storybook:build-import-map": "lage storybook:build-import-map", | ||||
|         "storybook:build-import-map-script": "lage storybook:build-import-map-script", | ||||
|         "storybook:run-import-map-script": "lage storybook:run-import-map-script" | ||||
|         "extract-locales": "lit-localize extract", | ||||
|         "build-locales": "run-s build-locales:build", | ||||
|         "build-locales:build": "lit-localize build", | ||||
|         "build-locales:repair": "prettier --write ./src/locale-codes.ts", | ||||
|         "rollup:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c ./rollup.config.mjs", | ||||
|         "rollup:build-proxy": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c ./rollup.proxy.mjs", | ||||
|         "rollup:watch": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c -w", | ||||
|         "build": "run-s build-locales rollup:build", | ||||
|         "build-proxy": "run-s build-locales rollup:build-proxy", | ||||
|         "watch": "run-s build-locales rollup:watch", | ||||
|         "lint": "eslint . --max-warnings 0 --fix", | ||||
|         "lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[M?][M?]' | cut -c8- | grep -E '\\.(ts|js|tsx|jsx)$') ", | ||||
|         "lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s", | ||||
|         "lit-analyse": "lit-analyzer src", | ||||
|         "precommit": "run-s tsc lit-analyse lint:precommit lint:spelling prettier", | ||||
|         "prequick": "run-s tsc:execute lit-analyse lint:precommit lint:spelling", | ||||
|         "prettier-check": "prettier --check .", | ||||
|         "prettier": "prettier --write .", | ||||
|         "pseudolocalize:build-extract-script": "cd scripts && tsc --esModuleInterop --module es2020 --moduleResolution 'node' pseudolocalize.ts && mv pseudolocalize.js pseudolocalize.mjs", | ||||
|         "pseudolocalize:extract": "node scripts/pseudolocalize.mjs", | ||||
|         "pseudolocalize": "run-s pseudolocalize:build-extract-script pseudolocalize:extract", | ||||
|         "tsc:execute": "tsc --noEmit -p .", | ||||
|         "tsc": "run-s build-locales tsc:execute", | ||||
|         "storybook": "storybook dev -p 6006", | ||||
|         "storybook:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' storybook build", | ||||
|         "storybook:build-import-map": "run-s storybook:build-import-map-script storybook:run-import-map-script", | ||||
|         "storybook:build-import-map-script": "cd scripts && tsc --esModuleInterop --module es2020 --target es2020 --moduleResolution 'node' build-storybook-import-maps.ts && mv build-storybook-import-maps.js build-storybook-import-maps.mjs", | ||||
|         "storybook:run-import-map-script": "node scripts/build-storybook-import-maps.mjs" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@manypkg/cli": "^0.21.1", | ||||
|         "lage": "^2.7.9" | ||||
|         "@codemirror/lang-html": "^6.4.8", | ||||
|         "@codemirror/lang-javascript": "^6.2.1", | ||||
|         "@codemirror/lang-python": "^6.1.4", | ||||
|         "@codemirror/lang-xml": "^6.0.2", | ||||
|         "@codemirror/legacy-modes": "^6.3.3", | ||||
|         "@codemirror/theme-one-dark": "^6.1.2", | ||||
|         "@formatjs/intl-listformat": "^7.5.5", | ||||
|         "@fortawesome/fontawesome-free": "^6.5.1", | ||||
|         "@goauthentik/api": "^2023.10.7-1707933453", | ||||
|         "@lit-labs/context": "^0.4.0", | ||||
|         "@lit-labs/task": "^3.1.0", | ||||
|         "@lit/localize": "^0.11.4", | ||||
|         "@open-wc/lit-helpers": "^0.6.0", | ||||
|         "@patternfly/elements": "^2.4.0", | ||||
|         "@patternfly/patternfly": "^4.224.2", | ||||
|         "@sentry/browser": "^7.101.0", | ||||
|         "@webcomponents/webcomponentsjs": "^2.8.0", | ||||
|         "base64-js": "^1.5.1", | ||||
|         "chart.js": "^4.4.1", | ||||
|         "chartjs-adapter-moment": "^1.0.1", | ||||
|         "codemirror": "^6.0.1", | ||||
|         "construct-style-sheets-polyfill": "^3.1.0", | ||||
|         "core-js": "^3.35.1", | ||||
|         "country-flag-icons": "^1.5.9", | ||||
|         "fuse.js": "^7.0.0", | ||||
|         "guacamole-common-js": "^1.5.0", | ||||
|         "lit": "^2.8.0", | ||||
|         "mermaid": "^10.8.0", | ||||
|         "rapidoc": "^9.3.4", | ||||
|         "style-mod": "^4.1.0", | ||||
|         "webcomponent-qr-code": "^1.2.0", | ||||
|         "yaml": "^2.3.4" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@babel/core": "^7.23.9", | ||||
|         "@babel/plugin-proposal-class-properties": "^7.18.6", | ||||
|         "@babel/plugin-proposal-decorators": "^7.23.9", | ||||
|         "@babel/plugin-transform-private-methods": "^7.23.3", | ||||
|         "@babel/plugin-transform-private-property-in-object": "^7.23.4", | ||||
|         "@babel/plugin-transform-runtime": "^7.23.9", | ||||
|         "@babel/preset-env": "^7.23.9", | ||||
|         "@babel/preset-typescript": "^7.23.3", | ||||
|         "@hcaptcha/types": "^1.0.3", | ||||
|         "@jackfranklin/rollup-plugin-markdown": "^0.4.0", | ||||
|         "@jeysal/storybook-addon-css-user-preferences": "^0.2.0", | ||||
|         "@lit/localize-tools": "^0.7.2", | ||||
|         "@rollup/plugin-babel": "^6.0.4", | ||||
|         "@rollup/plugin-commonjs": "^25.0.7", | ||||
|         "@rollup/plugin-node-resolve": "^15.2.3", | ||||
|         "@rollup/plugin-replace": "^5.0.5", | ||||
|         "@rollup/plugin-terser": "^0.4.4", | ||||
|         "@rollup/plugin-typescript": "^11.1.6", | ||||
|         "@spotlightjs/spotlight": "^1.2.12", | ||||
|         "@storybook/addon-essentials": "^7.6.15", | ||||
|         "@storybook/addon-links": "^7.6.15", | ||||
|         "@storybook/api": "^7.6.15", | ||||
|         "@storybook/blocks": "^7.6.4", | ||||
|         "@storybook/manager-api": "^7.6.15", | ||||
|         "@storybook/web-components": "^7.6.15", | ||||
|         "@storybook/web-components-vite": "^7.6.15", | ||||
|         "@trivago/prettier-plugin-sort-imports": "^4.3.0", | ||||
|         "@types/chart.js": "^2.9.41", | ||||
|         "@types/codemirror": "5.60.15", | ||||
|         "@types/grecaptcha": "^3.0.7", | ||||
|         "@types/guacamole-common-js": "1.5.2", | ||||
|         "@typescript-eslint/eslint-plugin": "^7.0.1", | ||||
|         "@typescript-eslint/parser": "^7.0.1", | ||||
|         "babel-plugin-macros": "^3.1.0", | ||||
|         "babel-plugin-tsconfig-paths": "^1.0.3", | ||||
|         "cross-env": "^7.0.3", | ||||
|         "eslint": "^8.56.0", | ||||
|         "eslint-config-google": "^0.14.0", | ||||
|         "eslint-plugin-custom-elements": "0.0.8", | ||||
|         "eslint-plugin-lit": "^1.11.0", | ||||
|         "eslint-plugin-sonarjs": "^0.24.0", | ||||
|         "eslint-plugin-storybook": "^0.6.15", | ||||
|         "github-slugger": "^2.0.0", | ||||
|         "lit-analyzer": "^2.0.3", | ||||
|         "npm-run-all": "^4.1.5", | ||||
|         "prettier": "^3.2.5", | ||||
|         "pseudolocale": "^2.0.0", | ||||
|         "pyright": "=1.1.338", | ||||
|         "react": "^18.2.0", | ||||
|         "react-dom": "^18.2.0", | ||||
|         "rollup": "^4.10.0", | ||||
|         "rollup-plugin-copy": "^3.5.0", | ||||
|         "rollup-plugin-cssimport": "^1.0.3", | ||||
|         "rollup-plugin-modify": "^3.0.0", | ||||
|         "rollup-plugin-postcss-lit": "^2.1.0", | ||||
|         "storybook": "^7.6.15", | ||||
|         "storybook-addon-mock": "^4.3.0", | ||||
|         "ts-lit-plugin": "^2.0.2", | ||||
|         "tslib": "^2.6.2", | ||||
|         "turnstile-types": "^1.2.0", | ||||
|         "typescript": "^5.3.3", | ||||
|         "vite-tsconfig-paths": "^4.3.1" | ||||
|     }, | ||||
|     "optionalDependencies": { | ||||
|         "@esbuild/darwin-arm64": "^0.20.0", | ||||
| @ -44,8 +138,5 @@ | ||||
|     }, | ||||
|     "engines": { | ||||
|         "node": ">=20" | ||||
|     }, | ||||
|     "workspaces": [ | ||||
|         "packages/authentik" | ||||
|     ] | ||||
|     } | ||||
| } | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	