Compare commits
	
		
			60 Commits
		
	
	
		
			version/20
			...
			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] | [bumpversion] | ||||||
| current_version = 2023.10.7 | current_version = 2024.2.4 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
|  | |||||||
							
								
								
									
										66
									
								
								.github/actions/docker-push-variables/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										66
									
								
								.github/actions/docker-push-variables/action.yml
									
									
									
									
										vendored
									
									
								
							| @ -11,6 +11,10 @@ inputs: | |||||||
|     description: "Docker image arch" |     description: "Docker image arch" | ||||||
|  |  | ||||||
| outputs: | outputs: | ||||||
|  |   shouldBuild: | ||||||
|  |     description: "Whether to build image or not" | ||||||
|  |     value: ${{ steps.ev.outputs.shouldBuild }} | ||||||
|  |  | ||||||
|   sha: |   sha: | ||||||
|     description: "sha" |     description: "sha" | ||||||
|     value: ${{ steps.ev.outputs.sha }} |     value: ${{ steps.ev.outputs.sha }} | ||||||
| @ -34,60 +38,10 @@ runs: | |||||||
|   steps: |   steps: | ||||||
|     - name: Generate config |     - name: Generate config | ||||||
|       id: ev |       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: | |       run: | | ||||||
|         """Helper script to get the actual branch name, docker safe""" |         python3 ${{ github.action_path }}/push_vars.py | ||||||
|         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}", |  | ||||||
|                     f"{name}:{version_family}", |  | ||||||
|                 ] |  | ||||||
|             if not prerelease: |  | ||||||
|                 image_tags += [f"{name}:latest"] |  | ||||||
|         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}", |  | ||||||
|                     f"{name}:gh-{safe_branch_name}{suffix}", |  | ||||||
|                 ] |  | ||||||
|  |  | ||||||
|         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) |  | ||||||
|  | |||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										10
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -70,7 +70,7 @@ jobs: | |||||||
|           cp authentik/lib/default.yml local.env.yml |           cp authentik/lib/default.yml local.env.yml | ||||||
|           cp -R .github .. |           cp -R .github .. | ||||||
|           cp -R scripts .. |           cp -R scripts .. | ||||||
|           git checkout version/$(python -c "from authentik import __version__; print(__version__)") |           git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1) | ||||||
|           rm -rf .github/ scripts/ |           rm -rf .github/ scripts/ | ||||||
|           mv ../.github ../scripts . |           mv ../.github ../scripts . | ||||||
|       - name: Setup authentik env (stable) |       - name: Setup authentik env (stable) | ||||||
| @ -219,7 +219,6 @@ jobs: | |||||||
|       # Needed to upload contianer images to ghcr.io |       # Needed to upload contianer images to ghcr.io | ||||||
|       packages: write |       packages: write | ||||||
|     timeout-minutes: 120 |     timeout-minutes: 120 | ||||||
|     if: "github.repository == 'goauthentik/authentik'" |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
| @ -231,10 +230,13 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/dev-server |           image-name: ghcr.io/goauthentik/dev-server | ||||||
|           image-arch: ${{ matrix.arch }} |           image-arch: ${{ matrix.arch }} | ||||||
|       - name: Login to Container Registry |       - name: Login to Container Registry | ||||||
|  |         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v3 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
| @ -250,7 +252,7 @@ jobs: | |||||||
|             GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }} |             GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }} | ||||||
|             GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }} |             GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }} | ||||||
|           tags: ${{ steps.ev.outputs.imageTags }} |           tags: ${{ steps.ev.outputs.imageTags }} | ||||||
|           push: true |           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|           build-args: | |           build-args: | | ||||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} |             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||||
|           cache-from: type=gha |           cache-from: type=gha | ||||||
| @ -272,6 +274,8 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/dev-server |           image-name: ghcr.io/goauthentik/dev-server | ||||||
|       - name: Comment on PR |       - name: Comment on PR | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -71,7 +71,6 @@ jobs: | |||||||
|     permissions: |     permissions: | ||||||
|       # Needed to upload contianer images to ghcr.io |       # Needed to upload contianer images to ghcr.io | ||||||
|       packages: write |       packages: write | ||||||
|     if: "github.repository == 'goauthentik/authentik'" |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
| @ -83,9 +82,12 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/dev-${{ matrix.type }} |           image-name: ghcr.io/goauthentik/dev-${{ matrix.type }} | ||||||
|       - name: Login to Container Registry |       - name: Login to Container Registry | ||||||
|  |         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v3 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
| @ -98,7 +100,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           tags: ${{ steps.ev.outputs.imageTags }} |           tags: ${{ steps.ev.outputs.imageTags }} | ||||||
|           file: ${{ matrix.type }}.Dockerfile |           file: ${{ matrix.type }}.Dockerfile | ||||||
|           push: true |           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|           build-args: | |           build-args: | | ||||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} |             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||||
|           platforms: linux/amd64,linux/arm64 |           platforms: linux/amd64,linux/arm64 | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -20,6 +20,8 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/server,beryju/authentik |           image-name: ghcr.io/goauthentik/server,beryju/authentik | ||||||
|       - name: Docker Login Registry |       - name: Docker Login Registry | ||||||
| @ -72,6 +74,8 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }} |           image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }} | ||||||
|       - name: make empty clients |       - name: make empty clients | ||||||
| @ -168,12 +172,14 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/server |           image-name: ghcr.io/goauthentik/server | ||||||
|       - name: Get static files from docker image |       - name: Get static files from docker image | ||||||
|         run: | |         run: | | ||||||
|           docker pull ghcr.io/goauthentik/server:${{ steps.ev.outputs.imageMainTag }} |           docker pull ${{ steps.ev.outputs.imageMainTag }} | ||||||
|           container=$(docker container create ghcr.io/goauthentik/server:${{ steps.ev.outputs.imageMainTag }}) |           container=$(docker container create ${{ steps.ev.outputs.imageMainTag }}) | ||||||
|           docker cp ${container}:web/ . |           docker cp ${container}:web/ . | ||||||
|       - name: Create a Sentry.io release |       - name: Create a Sentry.io release | ||||||
|         uses: getsentry/action-release@v1 |         uses: getsentry/action-release@v1 | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -32,6 +32,8 @@ jobs: | |||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
|         uses: ./.github/actions/docker-push-variables |         uses: ./.github/actions/docker-push-variables | ||||||
|         id: ev |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|         with: |         with: | ||||||
|           image-name: ghcr.io/goauthentik/server |           image-name: ghcr.io/goauthentik/server | ||||||
|       - name: Create Release |       - name: Create Release | ||||||
|  | |||||||
| @ -103,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/pip \ | ||||||
|     --mount=type=cache,target=/root/.cache/pypoetry \ |     --mount=type=cache,target=/root/.cache/pypoetry \ | ||||||
|     python -m venv /ak-root/venv/ && \ |     python -m venv /ak-root/venv/ && \ | ||||||
|     pip3 install --upgrade pip && \ |     bash -c "source ${VENV_PATH}/bin/activate && \ | ||||||
|     pip3 install poetry && \ |         pip3 install --upgrade pip && \ | ||||||
|     poetry install --only=main --no-ansi --no-interaction |         pip3 install poetry && \ | ||||||
|  |         poetry install --only=main --no-ansi --no-interaction --no-root" | ||||||
|  |  | ||||||
| # Stage 6: Run | # Stage 6: Run | ||||||
| FROM docker.io/python:3.12.2-slim-bookworm AS final-image | 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) | UID = $(shell id -u) | ||||||
| GID = $(shell id -g) | GID = $(shell id -g) | ||||||
| NPM_VERSION = $(shell python -m scripts.npm_version) | 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" | DOCKER_IMAGE ?= "authentik:test" | ||||||
|  |  | ||||||
| GEN_API_TS = "gen-ts-api" | GEN_API_TS = "gen-ts-api" | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| from os import environ | from os import environ | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| __version__ = "2023.10.7" | __version__ = "2024.2.4" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | 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""" |         """Get all capabilities this server instance supports""" | ||||||
|         caps = [] |         caps = [] | ||||||
|         deb_test = settings.DEBUG or settings.TEST |         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) |             caps.append(Capabilities.CAN_SAVE_MEDIA) | ||||||
|         for processor in get_context_processors(): |         for processor in get_context_processors(): | ||||||
|             if cap := processor.capability(): |             if cap := processor.capability(): | ||||||
|  | |||||||
| @ -10,13 +10,13 @@ from rest_framework.response import Response | |||||||
| from rest_framework.serializers import ListSerializer, ModelSerializer | from rest_framework.serializers import ListSerializer, ModelSerializer | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.blueprints.models import BlueprintInstance | from authentik.blueprints.models import BlueprintInstance | ||||||
| from authentik.blueprints.v1.importer import Importer | from authentik.blueprints.v1.importer import Importer | ||||||
| from authentik.blueprints.v1.oci import OCI_PREFIX | from authentik.blueprints.v1.oci import OCI_PREFIX | ||||||
| from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict | from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import JSONDictField, PassiveSerializer | from authentik.core.api.utils import JSONDictField, PassiveSerializer | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class ManagedSerializer: | class ManagedSerializer: | ||||||
|  | |||||||
| @ -74,7 +74,7 @@ class Exporter: | |||||||
|  |  | ||||||
|  |  | ||||||
| class FlowExporter(Exporter): | class FlowExporter(Exporter): | ||||||
|     """Exporter customised to only return objects related to `flow`""" |     """Exporter customized to only return objects related to `flow`""" | ||||||
|  |  | ||||||
|     flow: Flow |     flow: Flow | ||||||
|     with_policies: bool |     with_policies: bool | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ from sentry_sdk.hub import Hub | |||||||
|  |  | ||||||
| from authentik import get_full_version | from authentik import get_full_version | ||||||
| from authentik.brands.models import Brand | from authentik.brands.models import Brand | ||||||
|  | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
| _q_default = Q(default=True) | _q_default = Q(default=True) | ||||||
| DEFAULT_BRAND = Brand(domain="fallback") | 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]: | def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||||
|     """Context Processor that injects brand object into every template""" |     """Context Processor that injects brand object into every template""" | ||||||
|     brand = getattr(request, "brand", DEFAULT_BRAND) |     brand = getattr(request, "brand", DEFAULT_BRAND) | ||||||
|  |     tenant = getattr(request, "tenant", Tenant()) | ||||||
|     trace = "" |     trace = "" | ||||||
|     span = Hub.current.scope.span |     span = Hub.current.scope.span | ||||||
|     if span: |     if span: | ||||||
|         trace = span.to_traceparent() |         trace = span.to_traceparent() | ||||||
|     return { |     return { | ||||||
|         "brand": brand, |         "brand": brand, | ||||||
|         "footer_links": request.tenant.footer_links, |         "footer_links": tenant.footer_links, | ||||||
|         "sentry_trace": trace, |         "sentry_trace": trace, | ||||||
|         "version": get_full_version(), |         "version": get_full_version(), | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -23,7 +23,6 @@ from structlog.stdlib import get_logger | |||||||
| from structlog.testing import capture_logs | from structlog.testing import capture_logs | ||||||
|  |  | ||||||
| from authentik.admin.api.metrics import CoordinateSerializer | 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.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||||
| from authentik.core.api.providers import ProviderSerializer | from authentik.core.api.providers import ProviderSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | 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.api.exec import PolicyTestResultSerializer | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| from authentik.policies.types import PolicyResult | from authentik.policies.types import PolicyResult | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
| from authentik.rbac.filters import ObjectFilter | from authentik.rbac.filters import ObjectFilter | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | |||||||
| @ -15,11 +15,11 @@ from rest_framework.response import Response | |||||||
| from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError | from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError | ||||||
| from rest_framework.viewsets import ModelViewSet | 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.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import JSONDictField, PassiveSerializer | from authentik.core.api.utils import JSONDictField, PassiveSerializer | ||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.rbac.api.roles import RoleSerializer | from authentik.rbac.api.roles import RoleSerializer | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupMemberSerializer(ModelSerializer): | class GroupMemberSerializer(ModelSerializer): | ||||||
|  | |||||||
| @ -14,7 +14,6 @@ from rest_framework.response import Response | |||||||
| from rest_framework.serializers import ModelSerializer, SerializerMethodField | from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||||
| from rest_framework.viewsets import GenericViewSet | from rest_framework.viewsets import GenericViewSet | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.blueprints.api import ManagedSerializer | from authentik.blueprints.api import ManagedSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer | 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.events.utils import sanitize_item | ||||||
| from authentik.lib.utils.reflection import all_subclasses | from authentik.lib.utils.reflection import all_subclasses | ||||||
| from authentik.policies.api.exec import PolicyTestSerializer | from authentik.policies.api.exec import PolicyTestSerializer | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class PropertyMappingTestResultSerializer(PassiveSerializer): | class PropertyMappingTestResultSerializer(PassiveSerializer): | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions | from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer | 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.lib.utils.reflection import all_subclasses | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | |||||||
| @ -15,15 +15,15 @@ from rest_framework.serializers import ModelSerializer | |||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
| from authentik.api.authorization import OwnerSuperuserPermissions | from authentik.api.authorization import OwnerSuperuserPermissions | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.blueprints.api import ManagedSerializer | from authentik.blueprints.api import ManagedSerializer | ||||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.users import UserSerializer | from authentik.core.api.users import UserSerializer | ||||||
| from authentik.core.api.utils import PassiveSerializer | 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.models import Event, EventAction | ||||||
| from authentik.events.utils import model_to_dict | from authentik.events.utils import model_to_dict | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class TokenSerializer(ManagedSerializer, ModelSerializer): | class TokenSerializer(ManagedSerializer, ModelSerializer): | ||||||
| @ -36,6 +36,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): | |||||||
|         if SERIALIZER_CONTEXT_BLUEPRINT in self.context: |         if SERIALIZER_CONTEXT_BLUEPRINT in self.context: | ||||||
|             self.fields["key"] = CharField(required=False) |             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]: |     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: | ||||||
|         """Ensure only API or App password tokens are created.""" |         """Ensure only API or App password tokens are created.""" | ||||||
|         request: Request = self.context.get("request") |         request: Request = self.context.get("request") | ||||||
|  | |||||||
| @ -49,7 +49,6 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.admin.api.metrics import CoordinateSerializer | 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.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||||
| from authentik.brands.models import Brand | from authentik.brands.models import Brand | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| @ -74,6 +73,7 @@ from authentik.flows.models import FlowToken | |||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | from authentik.flows.views.executor import QS_KEY_TOKEN | ||||||
| from authentik.lib.avatars import get_avatar | 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.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| @ -154,7 +154,7 @@ class UserSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|     def get_avatar(self, user: User) -> str: |     def get_avatar(self, user: User) -> str: | ||||||
|         """User's avatar, either a http/https URL or a data URI""" |         """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: |     def validate_path(self, path: str) -> str: | ||||||
|         """Validate path""" |         """Validate path""" | ||||||
| @ -218,7 +218,7 @@ class UserSelfSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|     def get_avatar(self, user: User) -> str: |     def get_avatar(self, user: User) -> str: | ||||||
|         """User's avatar, either a http/https URL or a data URI""" |         """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( |     @extend_schema_field( | ||||||
|         ListSerializer( |         ListSerializer( | ||||||
| @ -533,7 +533,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|             400: OpenApiResponse(description="Bad request"), |             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: |     def set_password(self, request: Request, pk: int) -> Response: | ||||||
|         """Set password for user""" |         """Set password for user""" | ||||||
|         user: User = self.get_object() |         user: User = self.get_object() | ||||||
| @ -611,7 +611,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|         email_stage: EmailStage = stages.first() |         email_stage: EmailStage = stages.first() | ||||||
|         message = TemplateEmailMessage( |         message = TemplateEmailMessage( | ||||||
|             subject=_(email_stage.subject), |             subject=_(email_stage.subject), | ||||||
|             to=[for_user.email], |             to=[(for_user.name, for_user.email)], | ||||||
|             template_name=email_stage.template, |             template_name=email_stage.template, | ||||||
|             language=for_user.locale(request), |             language=for_user.locale(request), | ||||||
|             template_context={ |             template_context={ | ||||||
| @ -631,7 +631,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|             "401": OpenApiResponse(description="Access denied"), |             "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: |     def impersonate(self, request: Request, pk: int) -> Response: | ||||||
|         """Impersonate a user""" |         """Impersonate a user""" | ||||||
|         if not request.tenant.impersonation: |         if not request.tenant.impersonation: | ||||||
|  | |||||||
| @ -7,8 +7,8 @@ from guardian.shortcuts import get_anonymous_user | |||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.api.tokens import TokenSerializer | from authentik.core.api.tokens import TokenSerializer | ||||||
| from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User | from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents | ||||||
| from authentik.core.tests.utils import create_test_admin_user | from authentik.core.tests.utils import create_test_admin_user, create_test_user | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -17,7 +17,7 @@ class TestTokenAPI(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="testuser") |         self.user = create_test_user() | ||||||
|         self.admin = create_test_admin_user() |         self.admin = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
| @ -76,6 +76,24 @@ class TestTokenAPI(APITestCase): | |||||||
|         self.assertEqual(token.intent, TokenIntents.INTENT_API) |         self.assertEqual(token.intent, TokenIntents.INTENT_API) | ||||||
|         self.assertEqual(token.expiring, False) |         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): |     def test_list(self): | ||||||
|         """Test Token List (Test normal authentication)""" |         """Test Token List (Test normal authentication)""" | ||||||
|         Token.objects.all().delete() |         Token.objects.all().delete() | ||||||
|  | |||||||
| @ -24,13 +24,13 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.authorization import SecretKeyFilter | from authentik.api.authorization import SecretKeyFilter | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.crypto.apps import MANAGED_KEY | from authentik.crypto.apps import MANAGED_KEY | ||||||
| from authentik.crypto.builder import CertificateBuilder | from authentik.crypto.builder import CertificateBuilder | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,12 +16,12 @@ from rest_framework.response import Response | |||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import ModelViewSet | 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.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.core.models import User, UserTypes | from authentik.core.models import User, UserTypes | ||||||
| from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer | from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer | ||||||
| from authentik.enterprise.models import License | from authentik.enterprise.models import License | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
| from authentik.root.install_id import get_install_id | from authentik.root.install_id import get_install_id | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -31,7 +31,7 @@ class EnterpriseRequiredMixin: | |||||||
|  |  | ||||||
|     def validate(self, attrs: dict) -> dict: |     def validate(self, attrs: dict) -> dict: | ||||||
|         """Check that a valid license exists""" |         """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.")) |             raise ValidationError(_("Enterprise is required to create/update this object.")) | ||||||
|         return super().validate(attrs) |         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.db.models.signals import post_init | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
|  |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.events.middleware import AuditMiddleware, should_log_model | from authentik.events.middleware import AuditMiddleware, should_log_model | ||||||
| from authentik.events.utils import cleanse_dict, sanitize_item | from authentik.events.utils import cleanse_dict, sanitize_item | ||||||
|  |  | ||||||
| @ -28,13 +27,10 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | |||||||
|         super().connect(request) |         super().connect(request) | ||||||
|         if not self.enabled: |         if not self.enabled: | ||||||
|             return |             return | ||||||
|         user = getattr(request, "user", self.anonymous_user) |  | ||||||
|         if not user.is_authenticated: |  | ||||||
|             user = self.anonymous_user |  | ||||||
|         if not hasattr(request, "request_id"): |         if not hasattr(request, "request_id"): | ||||||
|             return |             return | ||||||
|         post_init.connect( |         post_init.connect( | ||||||
|             partial(self.post_init_handler, user=user, request=request), |             partial(self.post_init_handler, request=request), | ||||||
|             dispatch_uid=request.request_id, |             dispatch_uid=request.request_id, | ||||||
|             weak=False, |             weak=False, | ||||||
|         ) |         ) | ||||||
| @ -76,7 +72,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | |||||||
|                 diff[key] = {"previous_value": value, "new_value": after.get(key)} |                 diff[key] = {"previous_value": value, "new_value": after.get(key)} | ||||||
|         return sanitize_item(diff) |         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""" |         """post_init django model handler""" | ||||||
|         if not should_log_model(instance): |         if not should_log_model(instance): | ||||||
|             return |             return | ||||||
| @ -91,7 +87,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | |||||||
|     # pylint: disable=too-many-arguments |     # pylint: disable=too-many-arguments | ||||||
|     def post_save_handler( |     def post_save_handler( | ||||||
|         self, |         self, | ||||||
|         user: User, |  | ||||||
|         request: HttpRequest, |         request: HttpRequest, | ||||||
|         sender, |         sender, | ||||||
|         instance: Model, |         instance: Model, | ||||||
| @ -113,6 +108,4 @@ class EnterpriseAuditMiddleware(AuditMiddleware): | |||||||
|                 for field_set in ignored_field_sets: |                 for field_set in ignored_field_sets: | ||||||
|                     if set(diff.keys()) == set(field_set): |                     if set(diff.keys()) == set(field_set): | ||||||
|                         return None |                         return None | ||||||
|         return super().post_save_handler( |         return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_) | ||||||
|             user, request, sender, instance, created, thread_kwargs, **_ |  | ||||||
|         ) |  | ||||||
|  | |||||||
| @ -188,20 +188,21 @@ class LicenseKey: | |||||||
|  |  | ||||||
|     def summary(self) -> LicenseSummary: |     def summary(self) -> LicenseSummary: | ||||||
|         """Summary of license status""" |         """Summary of license status""" | ||||||
|  |         has_license = License.objects.all().count() > 0 | ||||||
|         last_valid = LicenseKey.last_valid_date() |         last_valid = LicenseKey.last_valid_date() | ||||||
|         show_admin_warning = last_valid < now() - timedelta(weeks=2) |         show_admin_warning = last_valid < now() - timedelta(weeks=2) | ||||||
|         show_user_warning = last_valid < now() - timedelta(weeks=4) |         show_user_warning = last_valid < now() - timedelta(weeks=4) | ||||||
|         read_only = last_valid < now() - timedelta(weeks=6) |         read_only = last_valid < now() - timedelta(weeks=6) | ||||||
|         latest_valid = datetime.fromtimestamp(self.exp) |         latest_valid = datetime.fromtimestamp(self.exp) | ||||||
|         return LicenseSummary( |         return LicenseSummary( | ||||||
|             show_admin_warning=show_admin_warning, |             show_admin_warning=show_admin_warning and has_license, | ||||||
|             show_user_warning=show_user_warning, |             show_user_warning=show_user_warning and has_license, | ||||||
|             read_only=read_only, |             read_only=read_only and has_license, | ||||||
|             latest_valid=latest_valid, |             latest_valid=latest_valid, | ||||||
|             internal_users=self.internal_users, |             internal_users=self.internal_users, | ||||||
|             external_users=self.external_users, |             external_users=self.external_users, | ||||||
|             valid=self.is_valid(), |             valid=self.is_valid(), | ||||||
|             has_license=License.objects.all().count() > 0, |             has_license=has_license, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|  | |||||||
| @ -6,13 +6,13 @@ from rest_framework.filters import OrderingFilter, SearchFilter | |||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import GenericViewSet | 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.groups import GroupMemberSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.enterprise.api import EnterpriseRequiredMixin | from authentik.enterprise.api import EnterpriseRequiredMixin | ||||||
| from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer | from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer | ||||||
| from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer | 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): | class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer): | ||||||
| @ -23,7 +23,7 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer): | |||||||
|     user = GroupMemberSerializer(source="session.user", read_only=True) |     user = GroupMemberSerializer(source="session.user", read_only=True) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Endpoint |         model = ConnectionToken | ||||||
|         fields = [ |         fields = [ | ||||||
|             "pk", |             "pk", | ||||||
|             "provider", |             "provider", | ||||||
| @ -49,5 +49,5 @@ class ConnectionTokenViewSet( | |||||||
|     filterset_fields = ["endpoint", "session__user", "provider"] |     filterset_fields = ["endpoint", "session__user", "provider"] | ||||||
|     search_fields = ["endpoint__name", "provider__name"] |     search_fields = ["endpoint__name", "provider__name"] | ||||||
|     ordering = ["endpoint__name", "provider__name"] |     ordering = ["endpoint__name", "provider__name"] | ||||||
|     permission_classes = [OwnerPermissions] |     permission_classes = [OwnerSuperuserPermissions] | ||||||
|     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] |     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||||
|  | |||||||
| @ -2,11 +2,14 @@ | |||||||
|  |  | ||||||
| from datetime import datetime | 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.dispatch import receiver | ||||||
| from django.utils.timezone import get_current_timezone | 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.models import License | ||||||
|  | from authentik.enterprise.tasks import enterprise_update_usage | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(pre_save, sender=License) | @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.internal_users = status.internal_users | ||||||
|     instance.external_users = status.external_users |     instance.external_users = status.external_users | ||||||
|     instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone()) |     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.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import ModelViewSet | 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.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.events.models import ( | from authentik.events.models import ( | ||||||
| @ -24,6 +23,7 @@ from authentik.events.models import ( | |||||||
|     TransportMode, |     TransportMode, | ||||||
| ) | ) | ||||||
| from authentik.events.utils import get_user | from authentik.events.utils import get_user | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class NotificationTransportSerializer(ModelSerializer): | class NotificationTransportSerializer(ModelSerializer): | ||||||
|  | |||||||
| @ -21,8 +21,8 @@ from rest_framework.serializers import ModelSerializer | |||||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | from rest_framework.viewsets import ReadOnlyModelViewSet | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.events.models import SystemTask, TaskStatus | from authentik.events.models import SystemTask, TaskStatus | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| @ -81,7 +81,7 @@ class SystemTaskViewSet(ReadOnlyModelViewSet): | |||||||
|             500: OpenApiResponse(description="Failed to retry task"), |             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: |     def run(self, request: Request, pk=None) -> Response: | ||||||
|         """Run task""" |         """Run task""" | ||||||
|         task: SystemTask = self.get_object() |         task: SystemTask = self.get_object() | ||||||
|  | |||||||
| @ -82,26 +82,29 @@ class AuditMiddleware: | |||||||
|  |  | ||||||
|         self.anonymous_user = get_anonymous_user() |         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): |     def connect(self, request: HttpRequest): | ||||||
|         """Connect signal for automatic logging""" |         """Connect signal for automatic logging""" | ||||||
|         self._ensure_fallback_user() |         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"): |         if not hasattr(request, "request_id"): | ||||||
|             return |             return | ||||||
|         post_save.connect( |         post_save.connect( | ||||||
|             partial(self.post_save_handler, user=user, request=request), |             partial(self.post_save_handler, request=request), | ||||||
|             dispatch_uid=request.request_id, |             dispatch_uid=request.request_id, | ||||||
|             weak=False, |             weak=False, | ||||||
|         ) |         ) | ||||||
|         pre_delete.connect( |         pre_delete.connect( | ||||||
|             partial(self.pre_delete_handler, user=user, request=request), |             partial(self.pre_delete_handler, request=request), | ||||||
|             dispatch_uid=request.request_id, |             dispatch_uid=request.request_id, | ||||||
|             weak=False, |             weak=False, | ||||||
|         ) |         ) | ||||||
|         m2m_changed.connect( |         m2m_changed.connect( | ||||||
|             partial(self.m2m_changed_handler, user=user, request=request), |             partial(self.m2m_changed_handler, request=request), | ||||||
|             dispatch_uid=request.request_id, |             dispatch_uid=request.request_id, | ||||||
|             weak=False, |             weak=False, | ||||||
|         ) |         ) | ||||||
| @ -147,7 +150,6 @@ class AuditMiddleware: | |||||||
|     # pylint: disable=too-many-arguments |     # pylint: disable=too-many-arguments | ||||||
|     def post_save_handler( |     def post_save_handler( | ||||||
|         self, |         self, | ||||||
|         user: User, |  | ||||||
|         request: HttpRequest, |         request: HttpRequest, | ||||||
|         sender, |         sender, | ||||||
|         instance: Model, |         instance: Model, | ||||||
| @ -158,16 +160,18 @@ class AuditMiddleware: | |||||||
|         """Signal handler for all object's post_save""" |         """Signal handler for all object's post_save""" | ||||||
|         if not should_log_model(instance): |         if not should_log_model(instance): | ||||||
|             return |             return | ||||||
|  |         user = self.get_user(request) | ||||||
|  |  | ||||||
|         action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED |         action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED | ||||||
|         thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) |         thread = EventNewThread(action, request, user=user, model=model_to_dict(instance)) | ||||||
|         thread.kwargs.update(thread_kwargs or {}) |         thread.kwargs.update(thread_kwargs or {}) | ||||||
|         thread.run() |         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""" |         """Signal handler for all object's pre_delete""" | ||||||
|         if not should_log_model(instance):  # pragma: no cover |         if not should_log_model(instance):  # pragma: no cover | ||||||
|             return |             return | ||||||
|  |         user = self.get_user(request) | ||||||
|  |  | ||||||
|         EventNewThread( |         EventNewThread( | ||||||
|             EventAction.MODEL_DELETED, |             EventAction.MODEL_DELETED, | ||||||
| @ -176,14 +180,13 @@ class AuditMiddleware: | |||||||
|             model=model_to_dict(instance), |             model=model_to_dict(instance), | ||||||
|         ).run() |         ).run() | ||||||
|  |  | ||||||
|     def m2m_changed_handler( |     def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_): | ||||||
|         self, user: User, request: HttpRequest, sender, instance: Model, action: str, **_ |  | ||||||
|     ): |  | ||||||
|         """Signal handler for all object's m2m_changed""" |         """Signal handler for all object's m2m_changed""" | ||||||
|         if action not in ["pre_add", "pre_remove", "post_clear"]: |         if action not in ["pre_add", "pre_remove", "post_clear"]: | ||||||
|             return |             return | ||||||
|         if not should_log_m2m(instance): |         if not should_log_m2m(instance): | ||||||
|             return |             return | ||||||
|  |         user = self.get_user(request) | ||||||
|  |  | ||||||
|         EventNewThread( |         EventNewThread( | ||||||
|             EventAction.MODEL_UPDATED, |             EventAction.MODEL_UPDATED, | ||||||
|  | |||||||
| @ -451,6 +451,13 @@ class NotificationTransport(SerializerModel): | |||||||
|  |  | ||||||
|     def send_email(self, notification: "Notification") -> list[str]: |     def send_email(self, notification: "Notification") -> list[str]: | ||||||
|         """Send notification via global email configuration""" |         """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: " |         subject_prefix = "authentik Notification: " | ||||||
|         context = { |         context = { | ||||||
|             "key_value": { |             "key_value": { | ||||||
| @ -480,7 +487,7 @@ class NotificationTransport(SerializerModel): | |||||||
|             } |             } | ||||||
|         mail = TemplateEmailMessage( |         mail = TemplateEmailMessage( | ||||||
|             subject=subject_prefix + context["title"], |             subject=subject_prefix + context["title"], | ||||||
|             to=[f"{notification.user.name} <{notification.user.email}>"], |             to=[(notification.user.name, notification.user.email)], | ||||||
|             language=notification.user.locale(), |             language=notification.user.locale(), | ||||||
|             template_name="email/event_notification.html", |             template_name="email/event_notification.html", | ||||||
|             template_context=context, |             template_context=context, | ||||||
|  | |||||||
| @ -88,8 +88,8 @@ class SystemTask(TenantTask): | |||||||
|                 "duration": max(perf_counter() - self._start_precise, 0), |                 "duration": max(perf_counter() - self._start_precise, 0), | ||||||
|                 "task_call_module": self.__module__, |                 "task_call_module": self.__module__, | ||||||
|                 "task_call_func": self.__name__, |                 "task_call_func": self.__name__, | ||||||
|                 "task_call_args": args, |                 "task_call_args": sanitize_item(args), | ||||||
|                 "task_call_kwargs": kwargs, |                 "task_call_kwargs": sanitize_item(kwargs), | ||||||
|                 "status": self._status, |                 "status": self._status, | ||||||
|                 "messages": sanitize_item(self._messages), |                 "messages": sanitize_item(self._messages), | ||||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), |                 "expires": now() + timedelta(hours=self.result_timeout_hours), | ||||||
| @ -113,8 +113,8 @@ class SystemTask(TenantTask): | |||||||
|                 "duration": max(perf_counter() - self._start_precise, 0), |                 "duration": max(perf_counter() - self._start_precise, 0), | ||||||
|                 "task_call_module": self.__module__, |                 "task_call_module": self.__module__, | ||||||
|                 "task_call_func": self.__name__, |                 "task_call_func": self.__name__, | ||||||
|                 "task_call_args": args, |                 "task_call_args": sanitize_item(args), | ||||||
|                 "task_call_kwargs": kwargs, |                 "task_call_kwargs": sanitize_item(kwargs), | ||||||
|                 "status": self._status, |                 "status": self._status, | ||||||
|                 "messages": sanitize_item(self._messages), |                 "messages": sanitize_item(self._messages), | ||||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), |                 "expires": now() + timedelta(hours=self.result_timeout_hours), | ||||||
|  | |||||||
| @ -3,9 +3,10 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | 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.core.tests.utils import create_test_admin_user | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestEventsMiddleware(APITestCase): | class TestEventsMiddleware(APITestCase): | ||||||
| @ -47,3 +48,30 @@ class TestEventsMiddleware(APITestCase): | |||||||
|                 context__model__name="test-delete", |                 context__model__name="test-delete", | ||||||
|             ).exists() |             ).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 rest_framework.viewsets import ModelViewSet | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.blueprints.v1.exporter import FlowExporter | from authentik.blueprints.v1.exporter import FlowExporter | ||||||
| from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer | from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| @ -33,6 +32,7 @@ from authentik.lib.utils.file import ( | |||||||
|     set_file_url, |     set_file_url, | ||||||
| ) | ) | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """flow views tests""" | """flow views tests""" | ||||||
|  |  | ||||||
| from unittest.mock import MagicMock, PropertyMock, patch | from unittest.mock import MagicMock, PropertyMock, patch | ||||||
|  | from urllib.parse import urlencode | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.test.client import RequestFactory | 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.planner import FlowPlan, FlowPlanner | ||||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView | from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView | ||||||
| from authentik.flows.tests import FlowTestCase | 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.lib.generators import generate_id | ||||||
| from authentik.policies.dummy.models import DummyPolicy | from authentik.policies.dummy.models import DummyPolicy | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| @ -121,16 +127,73 @@ class TestFlowExecutor(FlowTestCase): | |||||||
|         TO_STAGE_RESPONSE_MOCK, |         TO_STAGE_RESPONSE_MOCK, | ||||||
|     ) |     ) | ||||||
|     def test_invalid_flow_redirect(self): |     def test_invalid_flow_redirect(self): | ||||||
|         """Tests that an invalid flow still redirects""" |         """Test invalid flow with valid redirect destination""" | ||||||
|         flow = create_test_flow( |         flow = create_test_flow( | ||||||
|             FlowDesignation.AUTHENTICATION, |             FlowDesignation.AUTHENTICATION, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         dest = "/unique-string" |         dest = "/unique-string" | ||||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) |         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.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( |     @patch( | ||||||
|         "authentik.flows.views.executor.to_stage_response", |         "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.template.response import TemplateResponse | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.decorators import method_decorator | 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.decorators.clickjacking import xframe_options_sameorigin | ||||||
| from django.views.generic import View | from django.views.generic import View | ||||||
| from drf_spectacular.types import OpenApiTypes | from drf_spectacular.types import OpenApiTypes | ||||||
| @ -178,6 +179,8 @@ class FlowExecutorView(APIView): | |||||||
|                     self.cancel() |                     self.cancel() | ||||||
|                 self._logger.debug("f(exec): Continuing existing plan") |                 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 |             # Don't check session again as we've either already loaded the plan or we need to plan | ||||||
|             if not self.plan: |             if not self.plan: | ||||||
|                 request.session[SESSION_KEY_HISTORY] = [] |                 request.session[SESSION_KEY_HISTORY] = [] | ||||||
| @ -192,8 +195,6 @@ class FlowExecutorView(APIView): | |||||||
|                     # To match behaviour with loading an empty flow plan from cache, |                     # To match behaviour with loading an empty flow plan from cache, | ||||||
|                     # we don't show an error message here, but rather call _flow_done() |                     # we don't show an error message here, but rather call _flow_done() | ||||||
|                     return self._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 |             # We don't save the Plan after getting the next stage | ||||||
|             # as it hasn't been successfully passed yet |             # as it hasn't been successfully passed yet | ||||||
|             try: |             try: | ||||||
| @ -392,7 +393,11 @@ class FlowExecutorView(APIView): | |||||||
|             NEXT_ARG_NAME, "authentik_core:root-redirect" |             NEXT_ARG_NAME, "authentik_core:root-redirect" | ||||||
|         ) |         ) | ||||||
|         self.cancel() |         self.cancel() | ||||||
|         return to_stage_response(self.request, redirect_with_qs(next_param)) |         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: |     def stage_ok(self) -> HttpResponse: | ||||||
|         """Callback called by stages upon successful completion. |         """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.stdlib import get_logger | ||||||
| from structlog.testing import capture_logs | 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.applications import user_app_cache_key | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer | 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.models import Policy, PolicyBinding | ||||||
| from authentik.policies.process import PolicyProcess | from authentik.policies.process import PolicyProcess | ||||||
| from authentik.policies.types import CACHE_PREFIX, PolicyRequest | from authentik.policies.types import CACHE_PREFIX, PolicyRequest | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | |||||||
| @ -15,13 +15,13 @@ from rest_framework.request import Request | |||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.providers import ProviderSerializer | from authentik.core.api.providers import ProviderSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer | from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer | ||||||
| from authentik.core.models import Provider | from authentik.core.models import Provider | ||||||
| from authentik.providers.oauth2.id_token import IDToken | from authentik.providers.oauth2.id_token import IDToken | ||||||
| from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class OAuth2ProviderSerializer(ProviderSerializer): | class OAuth2ProviderSerializer(ProviderSerializer): | ||||||
|  | |||||||
| @ -36,8 +36,21 @@ class TestAuthorize(OAuthTestCase): | |||||||
|  |  | ||||||
|     def test_invalid_grant_type(self): |     def test_invalid_grant_type(self): | ||||||
|         """Test with invalid grant type""" |         """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): |         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) |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|     def test_invalid_client_id(self): |     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() |         state = generate_id() | ||||||
|         user = create_test_admin_user() |         user = create_test_admin_user() | ||||||
|         self.client.force_login(user) |         self.client.force_login(user) | ||||||
| @ -365,7 +383,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|                     "response_type": "id_token", |                     "response_type": "id_token", | ||||||
|                     "client_id": "test", |                     "client_id": "test", | ||||||
|                     "state": state, |                     "state": state, | ||||||
|                     "scope": "openid", |                     "scope": "openid test", | ||||||
|                     "redirect_uri": "http://localhost", |                     "redirect_uri": "http://localhost", | ||||||
|                     "nonce": generate_id(), |                     "nonce": generate_id(), | ||||||
|                 }, |                 }, | ||||||
| @ -390,6 +408,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             ) |             ) | ||||||
|             jwt = self.validate_jwt(token, provider) |             jwt = self.validate_jwt(token, provider) | ||||||
|             self.assertEqual(jwt["amr"], ["pwd"]) |             self.assertEqual(jwt["amr"], ["pwd"]) | ||||||
|  |             self.assertEqual(jwt["sub"], "foo") | ||||||
|             self.assertAlmostEqual( |             self.assertAlmostEqual( | ||||||
|                 jwt["exp"] - now().timestamp(), |                 jwt["exp"] - now().timestamp(), | ||||||
|                 expires, |                 expires, | ||||||
|  | |||||||
| @ -4,9 +4,10 @@ from urllib.parse import urlencode | |||||||
|  |  | ||||||
| from django.urls import reverse | 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.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow | ||||||
| from authentik.lib.generators import generate_id | 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.models import DeviceToken, OAuth2Provider | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
| from authentik.providers.oauth2.views.device_init import QS_KEY_CODE | 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}), |             + 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) | ||||||
|  | |||||||
| @ -121,44 +121,18 @@ class OAuthAuthorizationParams: | |||||||
|         redirect_uri = query_dict.get("redirect_uri", "") |         redirect_uri = query_dict.get("redirect_uri", "") | ||||||
|  |  | ||||||
|         response_type = query_dict.get("response_type", "") |         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 |         # Validate and check the response_mode against the predefined dict | ||||||
|         # Set to Query or Fragment if not defined in request |         # Set to Query or Fragment if not defined in request | ||||||
|         response_mode = query_dict.get("response_mode", False) |         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") |         max_age = query_dict.get("max_age") | ||||||
|         return OAuthAuthorizationParams( |         return OAuthAuthorizationParams( | ||||||
|             client_id=query_dict.get("client_id", ""), |             client_id=query_dict.get("client_id", ""), | ||||||
|             redirect_uri=redirect_uri, |             redirect_uri=redirect_uri, | ||||||
|             response_type=response_type, |             response_type=response_type, | ||||||
|             response_mode=response_mode, |             response_mode=response_mode, | ||||||
|             grant_type=grant_type, |             grant_type="", | ||||||
|             scope=set(query_dict.get("scope", "").split()), |             scope=set(query_dict.get("scope", "").split()), | ||||||
|             state=state, |             state=state, | ||||||
|             nonce=query_dict.get("nonce"), |             nonce=query_dict.get("nonce"), | ||||||
| @ -178,6 +152,7 @@ class OAuthAuthorizationParams: | |||||||
|             LOGGER.warning("Invalid client identifier", client_id=self.client_id) |             LOGGER.warning("Invalid client identifier", client_id=self.client_id) | ||||||
|             raise ClientIdError(client_id=self.client_id) |             raise ClientIdError(client_id=self.client_id) | ||||||
|         self.check_redirect_uri() |         self.check_redirect_uri() | ||||||
|  |         self.check_grant() | ||||||
|         self.check_scope(github_compat) |         self.check_scope(github_compat) | ||||||
|         self.check_nonce() |         self.check_nonce() | ||||||
|         self.check_code_challenge() |         self.check_code_challenge() | ||||||
| @ -186,6 +161,34 @@ class OAuthAuthorizationParams: | |||||||
|                 self.redirect_uri, "request_not_supported", self.grant_type, self.state |                 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): |     def check_redirect_uri(self): | ||||||
|         """Redirect URI validation.""" |         """Redirect URI validation.""" | ||||||
|         allowed_redirect_urls = self.provider.redirect_uris.split() |         allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
| @ -257,9 +260,9 @@ class OAuthAuthorizationParams: | |||||||
|         if SCOPE_OFFLINE_ACCESS in self.scope: |         if SCOPE_OFFLINE_ACCESS in self.scope: | ||||||
|             # https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess |             # https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess | ||||||
|             if PROMPT_CONSENT not in self.prompt: |             if PROMPT_CONSENT not in self.prompt: | ||||||
|                 raise AuthorizeError( |                 # Instead of ignoring the `offline_access` scope when `prompt` | ||||||
|                     self.redirect_uri, "consent_required", self.grant_type, self.state |                 # isn't set to `consent`, we set override it ourselves | ||||||
|                 ) |                 self.prompt.add(PROMPT_CONSENT) | ||||||
|             if self.response_type not in [ |             if self.response_type not in [ | ||||||
|                 ResponseTypes.CODE, |                 ResponseTypes.CODE, | ||||||
|                 ResponseTypes.CODE_TOKEN, |                 ResponseTypes.CODE_TOKEN, | ||||||
|  | |||||||
| @ -12,10 +12,11 @@ from django.views.decorators.csrf import csrf_exempt | |||||||
| from rest_framework.throttling import AnonRateThrottle | from rest_framework.throttling import AnonRateThrottle | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider | 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() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| @ -38,7 +39,9 @@ class DeviceView(View): | |||||||
|         ).first() |         ).first() | ||||||
|         if not provider: |         if not provider: | ||||||
|             return HttpResponseBadRequest() |             return HttpResponseBadRequest() | ||||||
|         if not get_application(provider): |         try: | ||||||
|  |             _ = provider.application | ||||||
|  |         except Application.DoesNotExist: | ||||||
|             return HttpResponseBadRequest() |             return HttpResponseBadRequest() | ||||||
|         self.provider = provider |         self.provider = provider | ||||||
|         self.client_id = client_id |         self.client_id = client_id | ||||||
|  | |||||||
| @ -1,11 +1,10 @@ | |||||||
| """Device flow views""" | """Device flow views""" | ||||||
|  |  | ||||||
| from typing import Optional | from typing import Any, Optional | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from django.views import View | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.exceptions import ErrorDetail |  | ||||||
| from rest_framework.fields import CharField, IntegerField | from rest_framework.fields import CharField, IntegerField | ||||||
| from structlog.stdlib import get_logger | 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.stage import ChallengeStageView | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | 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.models import DeviceToken, OAuth2Provider | ||||||
| from authentik.providers.oauth2.views.device_finish import ( | from authentik.providers.oauth2.views.device_finish import ( | ||||||
|     PLAN_CONTEXT_DEVICE, |     PLAN_CONTEXT_DEVICE, | ||||||
| @ -44,48 +44,52 @@ def get_application(provider: OAuth2Provider) -> Optional[Application]: | |||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]: | class CodeValidatorView(PolicyAccessView): | ||||||
|     """Validate user token""" |     """Helper to validate frontside token""" | ||||||
|     token = DeviceToken.objects.filter( |  | ||||||
|         user_code=code, |  | ||||||
|     ).first() |  | ||||||
|     if not token: |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     app = get_application(token.provider) |     def __init__(self, code: str, **kwargs: Any) -> None: | ||||||
|     if not app: |         super().__init__(**kwargs) | ||||||
|         return None |         self.code = code | ||||||
|  |  | ||||||
|     scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider) |     def resolve_provider_application(self): | ||||||
|     planner = FlowPlanner(token.provider.authorization_flow) |         self.token = DeviceToken.objects.filter(user_code=self.code).first() | ||||||
|     planner.allow_empty_flows = True |         if not self.token: | ||||||
|     try: |             raise Application.DoesNotExist | ||||||
|         plan = planner.plan( |         self.provider = self.token.provider | ||||||
|             request, |         self.application = self.token.provider.application | ||||||
|             { |  | ||||||
|                 PLAN_CONTEXT_SSO: True, |     def get(self, request: HttpRequest, *args, **kwargs): | ||||||
|                 PLAN_CONTEXT_APPLICATION: app, |         scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider) | ||||||
|                 # OAuth2 related params |         planner = FlowPlanner(self.provider.authorization_flow) | ||||||
|                 PLAN_CONTEXT_DEVICE: token, |         planner.allow_empty_flows = True | ||||||
|                 # Consent related params |         planner.use_cache = False | ||||||
|                 PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.") |         try: | ||||||
|                 % {"application": app.name}, |             plan = planner.plan( | ||||||
|                 PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions, |                 request, | ||||||
|             }, |                 { | ||||||
|  |                     PLAN_CONTEXT_SSO: True, | ||||||
|  |                     PLAN_CONTEXT_APPLICATION: self.application, | ||||||
|  |                     # OAuth2 related params | ||||||
|  |                     PLAN_CONTEXT_DEVICE: self.token, | ||||||
|  |                     # Consent related params | ||||||
|  |                     PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.") | ||||||
|  |                     % {"application": self.application.name}, | ||||||
|  |                     PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |         except FlowNonApplicableException: | ||||||
|  |             LOGGER.warning("Flow not applicable to user") | ||||||
|  |             return None | ||||||
|  |         plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) | ||||||
|  |         request.session[SESSION_KEY_PLAN] = plan | ||||||
|  |         return redirect_with_qs( | ||||||
|  |             "authentik_core:if-flow", | ||||||
|  |             request.GET, | ||||||
|  |             flow_slug=self.token.provider.authorization_flow.slug, | ||||||
|         ) |         ) | ||||||
|     except FlowNonApplicableException: |  | ||||||
|         LOGGER.warning("Flow not applicable to user") |  | ||||||
|         return None |  | ||||||
|     plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) |  | ||||||
|     request.session[SESSION_KEY_PLAN] = plan |  | ||||||
|     return redirect_with_qs( |  | ||||||
|         "authentik_core:if-flow", |  | ||||||
|         request.GET, |  | ||||||
|         flow_slug=token.provider.authorization_flow.slug, |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class DeviceEntryView(View): | class DeviceEntryView(PolicyAccessView): | ||||||
|     """View used to initiate the device-code flow, url entered by endusers""" |     """View used to initiate the device-code flow, url entered by endusers""" | ||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: |     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||||
| @ -95,7 +99,9 @@ class DeviceEntryView(View): | |||||||
|             LOGGER.info("Brand has no device code flow configured", brand=brand) |             LOGGER.info("Brand has no device code flow configured", brand=brand) | ||||||
|             return HttpResponse(status=404) |             return HttpResponse(status=404) | ||||||
|         if QS_KEY_CODE in request.GET: |         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: |             if validation: | ||||||
|                 return validation |                 return validation | ||||||
|             LOGGER.info("Got code from query parameter but no matching token found") |             LOGGER.info("Got code from query parameter but no matching token found") | ||||||
| @ -130,6 +136,13 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse): | |||||||
|     code = IntegerField() |     code = IntegerField() | ||||||
|     component = CharField(default="ak-provider-oauth2-device-code") |     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): | class OAuthDeviceCodeStage(ChallengeStageView): | ||||||
|     """Flow challenge for users to enter device codes""" |     """Flow challenge for users to enter device codes""" | ||||||
| @ -145,12 +158,4 @@ class OAuthDeviceCodeStage(ChallengeStageView): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: |     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: | ||||||
|         code = response.validated_data["code"] |         return 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 |  | ||||||
|  | |||||||
| @ -101,8 +101,8 @@ class UserInfoView(View): | |||||||
|                     value=value, |                     value=value, | ||||||
|                 ) |                 ) | ||||||
|                 continue |                 continue | ||||||
|             LOGGER.debug("updated scope", scope=scope) |  | ||||||
|             always_merger.merge(final_claims, value) |             always_merger.merge(final_claims, value) | ||||||
|  |             LOGGER.debug("updated scope", scope=scope) | ||||||
|         return final_claims |         return final_claims | ||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: |     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||||
| @ -121,8 +121,9 @@ class UserInfoView(View): | |||||||
|         """Handle GET Requests for UserInfo""" |         """Handle GET Requests for UserInfo""" | ||||||
|         if not self.token: |         if not self.token: | ||||||
|             return HttpResponseBadRequest() |             return HttpResponseBadRequest() | ||||||
|         claims = self.get_claims(self.token.provider, self.token) |         claims = {} | ||||||
|         claims["sub"] = self.token.id_token.sub |         claims.setdefault("sub", self.token.id_token.sub) | ||||||
|  |         claims.update(self.get_claims(self.token.provider, self.token)) | ||||||
|         if self.token.id_token.nonce: |         if self.token.id_token.nonce: | ||||||
|             claims["nonce"] = self.token.id_token.nonce |             claims["nonce"] = self.token.id_token.nonce | ||||||
|         response = TokenResponse(claims) |         response = TokenResponse(claims) | ||||||
|  | |||||||
| @ -22,7 +22,6 @@ from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError | |||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.providers import ProviderSerializer | from authentik.core.api.providers import ProviderSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer | 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.authn_request_parser import AuthNRequest | ||||||
| from authentik.providers.saml.processors.metadata import MetadataProcessor | from authentik.providers.saml.processors.metadata import MetadataProcessor | ||||||
| from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser | 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 | from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | |||||||
| @ -15,10 +15,10 @@ from rest_framework.response import Response | |||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import GenericViewSet | from rest_framework.viewsets import GenericViewSet | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.policies.event_matcher.models import model_choices | from authentik.policies.event_matcher.models import model_choices | ||||||
| from authentik.rbac.api.rbac import PermissionAssignSerializer | from authentik.rbac.api.rbac import PermissionAssignSerializer | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
| from authentik.rbac.models import Role | 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.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import GenericViewSet | from rest_framework.viewsets import GenericViewSet | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.groups import GroupMemberSerializer | from authentik.core.api.groups import GroupMemberSerializer | ||||||
| from authentik.core.models import User, UserTypes | from authentik.core.models import User, UserTypes | ||||||
| from authentik.policies.event_matcher.models import model_choices | from authentik.policies.event_matcher.models import model_choices | ||||||
| from authentik.rbac.api.rbac import PermissionAssignSerializer | from authentik.rbac.api.rbac import PermissionAssignSerializer | ||||||
|  | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserObjectPermissionSerializer(ModelSerializer): | class UserObjectPermissionSerializer(ModelSerializer): | ||||||
|  | |||||||
| @ -14,18 +14,23 @@ LOGGER = get_logger() | |||||||
| def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[list[str]] = None): | def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[list[str]] = None): | ||||||
|     """Check permissions for a single custom action""" |     """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""" |         """Check permissions for a single custom action""" | ||||||
| 
 | 
 | ||||||
|         @wraps(func) |         @wraps(func) | ||||||
|         def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: |         def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: | ||||||
|             if obj_perm: |             if obj_perm: | ||||||
|                 obj = self.get_object() |                 _check_obj_perm(self, request) | ||||||
|                 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) |  | ||||||
|             if global_perms: |             if global_perms: | ||||||
|                 for other_perm in global_perms: |                 for other_perm in global_perms: | ||||||
|                     if not request.user.has_perm(other_perm): |                     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 | ||||||
| 
 | 
 | ||||||
|     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 | from authentik.lib.config import CONFIG | ||||||
|  |  | ||||||
|  | QUERY = """SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;""" | ||||||
|  |  | ||||||
|  |  | ||||||
| @lru_cache | @lru_cache | ||||||
| def get_install_id() -> str: | def get_install_id() -> str: | ||||||
| @ -18,7 +20,7 @@ def get_install_id() -> str: | |||||||
|     if settings.TEST: |     if settings.TEST: | ||||||
|         return str(uuid4()) |         return str(uuid4()) | ||||||
|     with connection.cursor() as cursor: |     with connection.cursor() as cursor: | ||||||
|         cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;") |         cursor.execute(QUERY) | ||||||
|         return cursor.fetchone()[0] |         return cursor.fetchone()[0] | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -38,5 +40,5 @@ def get_install_id_raw(): | |||||||
|         sslkey=CONFIG.get("postgresql.sslkey"), |         sslkey=CONFIG.get("postgresql.sslkey"), | ||||||
|     ) |     ) | ||||||
|     cursor = conn.cursor() |     cursor = conn.cursor() | ||||||
|     cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;") |     cursor.execute(QUERY) | ||||||
|     return cursor.fetchone()[0] |     return cursor.fetchone()[0] | ||||||
|  | |||||||
| @ -481,13 +481,6 @@ def _update_settings(app_path: str): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
| # Load subapps's settings |  | ||||||
| for _app in set(SHARED_APPS + TENANT_APPS): |  | ||||||
|     if not _app.startswith("authentik"): |  | ||||||
|         continue |  | ||||||
|     _update_settings(f"{_app}.settings") |  | ||||||
| _update_settings("data.user_settings") |  | ||||||
|  |  | ||||||
| if DEBUG: | if DEBUG: | ||||||
|     CELERY["task_always_eager"] = True |     CELERY["task_always_eager"] = True | ||||||
|     os.environ[ENV_GIT_HASH_KEY] = "dev" |     os.environ[ENV_GIT_HASH_KEY] = "dev" | ||||||
| @ -512,5 +505,13 @@ except ImportError: | |||||||
| # being imported for @prefill_task | # being imported for @prefill_task | ||||||
| TENANT_APPS.append("authentik.events") | TENANT_APPS.append("authentik.events") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Load subapps's settings | ||||||
|  | for _app in set(SHARED_APPS + TENANT_APPS): | ||||||
|  |     if not _app.startswith("authentik"): | ||||||
|  |         continue | ||||||
|  |     _update_settings(f"{_app}.settings") | ||||||
|  | _update_settings("data.user_settings") | ||||||
|  |  | ||||||
| SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | ||||||
| INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | ||||||
|  | |||||||
| @ -13,12 +13,12 @@ from rest_framework.serializers import ValidationError | |||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.sources import SourceSerializer | from authentik.core.api.sources import SourceSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.flows.challenge import RedirectChallenge | from authentik.flows.challenge import RedirectChallenge | ||||||
| from authentik.flows.views.executor import to_stage_response | 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.models import PlexSource, PlexSourceConnection | ||||||
| from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager | 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 structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.authorization import OwnerFilter, OwnerPermissions | from authentik.api.authorization import OwnerFilter, OwnerPermissions | ||||||
| from authentik.api.decorators import permission_required |  | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.flows.api.stages import StageSerializer | 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.models import AuthenticatorDuoStage, DuoDevice | ||||||
| from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL | from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL | ||||||
| from authentik.stages.authenticator_duo.tasks import duo_import_devices | 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( |     authentication_options = generate_authentication_options( | ||||||
|         rp_id=get_rp_id(request), |         rp_id=get_rp_id(request), | ||||||
|         allow_credentials=[], |         allow_credentials=[], | ||||||
|         user_verification=stage.webauthn_user_verification, |         user_verification=UserVerificationRequirement(stage.webauthn_user_verification), | ||||||
|     ) |     ) | ||||||
|     request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge |     request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge | ||||||
|  |  | ||||||
|  | |||||||
| @ -164,8 +164,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase): | |||||||
|         """Test webauthn (userless)""" |         """Test webauthn (userless)""" | ||||||
|         request = get_request("/") |         request = get_request("/") | ||||||
|         stage = AuthenticatorValidateStage.objects.create( |         stage = AuthenticatorValidateStage.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED | ||||||
|         ) |         ) | ||||||
|  |         stage.refresh_from_db() | ||||||
|         WebAuthnDevice.objects.create( |         WebAuthnDevice.objects.create( | ||||||
|             user=self.user, |             user=self.user, | ||||||
|             public_key=( |             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.bytes_to_base64url import bytes_to_base64url | ||||||
| from webauthn.helpers.exceptions import InvalidRegistrationResponse | from webauthn.helpers.exceptions import InvalidRegistrationResponse | ||||||
| from webauthn.helpers.structs import ( | from webauthn.helpers.structs import ( | ||||||
|  |     AuthenticatorAttachment, | ||||||
|     AuthenticatorSelectionCriteria, |     AuthenticatorSelectionCriteria, | ||||||
|     PublicKeyCredentialCreationOptions, |     PublicKeyCredentialCreationOptions, | ||||||
|     ResidentKeyRequirement, |     ResidentKeyRequirement, | ||||||
| @ -91,7 +92,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|         # set, cast it to string to ensure it's not a django class |         # set, cast it to string to ensure it's not a django class | ||||||
|         authenticator_attachment = stage.authenticator_attachment |         authenticator_attachment = stage.authenticator_attachment | ||||||
|         if authenticator_attachment: |         if authenticator_attachment: | ||||||
|             authenticator_attachment = str(authenticator_attachment) |             authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment)) | ||||||
|  |  | ||||||
|         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( |         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( | ||||||
|             rp_id=get_rp_id(self.request), |             rp_id=get_rp_id(self.request), | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ class Command(TenantCommand): | |||||||
|             delete_stage = True |             delete_stage = True | ||||||
|         message = TemplateEmailMessage( |         message = TemplateEmailMessage( | ||||||
|             subject="authentik Test-Email", |             subject="authentik Test-Email", | ||||||
|             to=[options["to"]], |             to=[("", options["to"])], | ||||||
|             template_name="email/setup.html", |             template_name="email/setup.html", | ||||||
|             template_context={}, |             template_context={}, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -111,7 +111,7 @@ class EmailStageView(ChallengeStageView): | |||||||
|         try: |         try: | ||||||
|             message = TemplateEmailMessage( |             message = TemplateEmailMessage( | ||||||
|                 subject=_(current_stage.subject), |                 subject=_(current_stage.subject), | ||||||
|                 to=[f"{pending_user.name} <{email}>"], |                 to=[(pending_user.name, email)], | ||||||
|                 language=pending_user.locale(self.request), |                 language=pending_user.locale(self.request), | ||||||
|                 template_name=current_stage.template, |                 template_name=current_stage.template, | ||||||
|                 template_context={ |                 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." %} | {% 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. | 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:" %} | {% translate "The following notification was created:" %} | ||||||
|  |  | ||||||
| @ -16,3 +16,4 @@ This email was sent from the notification transport {{ name }}. | |||||||
|  |  | ||||||
| --  | --  | ||||||
| Powered by goauthentik.io. | 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 %} | {% blocktrans %} | ||||||
| You recently requested to change your password for your authentik account. Use the link below to set a new password. | 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. | Powered by goauthentik.io. | ||||||
|  | {% endautoescape %} | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| {% load i18n %}authentik Test-Email | {% load i18n %}{% autoescape off %}authentik Test-Email | ||||||
| {% blocktrans %} | {% blocktrans %} | ||||||
| This is a test email to inform you, that you've successfully configured authentik emails. | This is a test email to inform you, that you've successfully configured authentik emails. | ||||||
| {% endblocktrans %} | {% endblocktrans %} | ||||||
|  |  | ||||||
| --  | --  | ||||||
| Powered by goauthentik.io. | Powered by goauthentik.io. | ||||||
|  | {% endautoescape %} | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ class TestEmailStageSending(FlowTestCase): | |||||||
|         session = self.client.session |         session = self.client.session | ||||||
|         session[SESSION_KEY_PLAN] = plan |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
|  |         Event.objects.filter(action=EventAction.EMAIL_SENT).delete() | ||||||
|  |  | ||||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) |         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||||
|         with patch( |         with patch( | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ from unittest.mock import PropertyMock, patch | |||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.mail.backends.locmem import EmailBackend | from django.core.mail.backends.locmem import EmailBackend | ||||||
|  | from django.core.mail.message import sanitize_address | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| @ -19,6 +20,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | |||||||
| from authentik.flows.tests import FlowTestCase | from authentik.flows.tests import FlowTestCase | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.email.models import EmailStage, get_template_choices | 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]: | 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" |                     event.context["message"], "Exception occurred while rendering E-mail template" | ||||||
|                 ) |                 ) | ||||||
|                 self.assertEqual(event.context["template"], "invalid.html") |                 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): | class TemplateEmailMessage(EmailMultiAlternatives): | ||||||
|     """Wrapper around EmailMultiAlternatives with integrated template rendering""" |     """Wrapper around EmailMultiAlternatives with integrated template rendering""" | ||||||
|  |  | ||||||
|     def __init__(self, template_name=None, template_context=None, language="", **kwargs): |     def __init__( | ||||||
|         super().__init__(**kwargs) |         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): |         with translation.override(language): | ||||||
|             html_content = render_to_string(template_name, template_context) |             html_content = render_to_string(template_name, template_context) | ||||||
|             try: |             try: | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from rest_framework.exceptions import ValidationError | |||||||
| from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER | from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER | ||||||
| from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes | from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes | ||||||
| from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION | 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.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.flows.views.executor import FlowExecutorView | 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 |         # this is just a sanity check to ensure that is removed | ||||||
|         if parts[0] == "attributes": |         if parts[0] == "attributes": | ||||||
|             parts = parts[1:] |             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]: |     def ensure_user(self) -> tuple[Optional[User], bool]: | ||||||
|         """Ensure a user exists""" |         """Ensure a user exists""" | ||||||
|  | |||||||
| @ -87,11 +87,6 @@ class Tenant(TenantMixin, SerializerModel): | |||||||
|             raise IntegrityError("Cannot create schema named template") |             raise IntegrityError("Cannot create schema named template") | ||||||
|         super().save(*args, **kwargs) |         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 |     @property | ||||||
|     def serializer(self) -> Serializer: |     def serializer(self) -> Serializer: | ||||||
|         from authentik.tenants.api.tenants import TenantSerializer |         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: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   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 |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -53,7 +53,7 @@ services: | |||||||
|       - postgresql |       - postgresql | ||||||
|       - redis |       - redis | ||||||
|   worker: |   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 |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -50,12 +50,12 @@ type StorageConfig struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type StorageMediaConfig 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"` | 	File    StorageFileConfig `yaml:"file"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type StorageFileConfig struct { | 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 { | type ErrorReportingConfig struct { | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	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 { | function prepare_debug { | ||||||
|  |     source ${VENV_PATH}/bin/activate | ||||||
|     poetry install --no-ansi --no-interaction |     poetry install --no-ansi --no-interaction | ||||||
|     touch /unittest.xml |     touch /unittest.xml | ||||||
|     chown authentik:authentik /unittest.xml |     chown authentik:authentik /unittest.xml | ||||||
| @ -86,6 +87,7 @@ elif [[ "$1" == "bash" ]]; then | |||||||
|     /bin/bash |     /bin/bash | ||||||
| elif [[ "$1" == "test-all" ]]; then | elif [[ "$1" == "test-all" ]]; then | ||||||
|     prepare_debug |     prepare_debug | ||||||
|  |     chmod 777 /root | ||||||
|     check_if_root "python -m manage test authentik" |     check_if_root "python -m manage test authentik" | ||||||
| elif [[ "$1" == "healthcheck" ]]; then | elif [[ "$1" == "healthcheck" ]]; then | ||||||
|     run_authentik healthcheck $(cat $MODE_FILE) |     run_authentik healthcheck $(cat $MODE_FILE) | ||||||
|  | |||||||
| @ -64,6 +64,7 @@ def release_lock(cursor: Cursor): | |||||||
|     """Release database lock""" |     """Release database lock""" | ||||||
|     if not LOCKED: |     if not LOCKED: | ||||||
|         return |         return | ||||||
|  |     LOGGER.info("releasing database lock") | ||||||
|     cursor.execute("SELECT pg_advisory_unlock(%s)", (ADV_LOCK_UID,)) |     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;") | ||||||
							
								
								
									
										188
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										188
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -402,33 +402,33 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "black" | name = "black" | ||||||
| version = "24.1.1" | version = "24.2.0" | ||||||
| description = "The uncompromising code formatter." | description = "The uncompromising code formatter." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, |     {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, | ||||||
|     {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, |     {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, | ||||||
|     {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, |     {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, | ||||||
|     {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, |     {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, | ||||||
|     {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, |     {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, | ||||||
|     {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, |     {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, | ||||||
|     {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, |     {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, | ||||||
|     {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, |     {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, | ||||||
|     {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, |     {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, | ||||||
|     {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, |     {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, | ||||||
|     {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, |     {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, | ||||||
|     {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, |     {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, | ||||||
|     {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, |     {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, | ||||||
|     {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, |     {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, | ||||||
|     {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, |     {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, | ||||||
|     {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, |     {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, | ||||||
|     {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, |     {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, | ||||||
|     {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, |     {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, | ||||||
|     {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, |     {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, | ||||||
|     {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, |     {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, | ||||||
|     {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, |     {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, | ||||||
|     {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, |     {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -506,48 +506,48 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "cbor2" | name = "cbor2" | ||||||
| version = "5.5.1" | version = "5.6.2" | ||||||
| description = "CBOR (de)serializer with extensive tag support" | description = "CBOR (de)serializer with extensive tag support" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:37ba4f719384bd4ea317e92a8763ea343e205f3112c8241778fd9dbc64ae1498"}, |     {file = "cbor2-5.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:516b8390936bb172ff18d7b609a452eaa51991513628949b0a9bf25cbe5a7129"}, | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:425ae919120b9d05b4794b3e5faf6584fc47a9d61db059d4f00ce16ae93a3f63"}, |     {file = "cbor2-5.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b8b504b590367a51fe8c0d9b8cb458a614d782d37b24483097e2b1e93ed0fff"}, | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c511ff6356d6f4292ced856d5048a24ee61a85634816f29dadf1f089e8cb4f9"}, |     {file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f687e6731b1198811223576800258a712ddbfdcfa86c0aee2cc8269193e6b96"}, | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6ab54a9282dd99a3a70d0f64706d3b3592e7920564a93101caa74dec322346c"}, |     {file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e94043d99fe779f62a15a5e156768588a2a7047bb3a127fa312ac1135ff5ecb"}, | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:39d94852dd61bda5b3d2bfe74e7b194a7199937d270f90099beec3e7584f0c9b"}, |     {file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8af7162fcf7aa2649f02563bdb18b2fa6478b751eee4df0257bffe19ea8f107a"}, | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65532ba929beebe1c63317ad00c79d4936b60a5c29a3c329d2aa7df4e72ad907"}, |     {file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ea7ecd81c5c6e02c2635973f52a0dd1e19c0bf5ef51f813d8cd5e3e7ed072726"}, | ||||||
|     {file = "cbor2-5.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1206180f66a9ad23e692cf457610c877f186ad303a1264b6c5335015b7bee83e"}, |     {file = "cbor2-5.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3c7f223f1fedc74d33f363d184cb2bab9e4bdf24998f73b5e3bef366d6c41628"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:42155a20be46312fad2ceb85a408e2d90da059c2d36a65e0b99abca57c5357fd"}, |     {file = "cbor2-5.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ea9e150029c3976c46ee9870b6dcdb0a5baae21008fe3290564886b11aa2b64"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f3827ae14c009df9b37790f1da5cd1f9d64f7ffec472a49ebf865c0af6b77e9"}, |     {file = "cbor2-5.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:922e06710e5cf6f56b82b0b90d2f356aa229b99e570994534206985f675fd307"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bfa417dbb8b4581ad3c2312469899518596551cfb0fe5bdaf8a6921cff69d7e"}, |     {file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b01a718e083e6de8b43296c3ccdb3aa8af6641f6bbb3ea1700427c6af73db28a"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3317e7dfb4f3180be90bcd853204558d89f119b624c2168153b53dea305e79d"}, |     {file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac85eb731c524d148f608b9bdb2069fa79e374a10ed5d10a2405eba9a6561e60"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a5770bdf4340de55679efe6c38fc6d64529fda547e7a85eb0217a82717a8235"}, |     {file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03e5b68867b9d89ff2abd14ef7c6d42fbd991adc3e734a19a294935f22a4d05a"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b5d53826ad0c92fcb004b2a475896610b51e0ca010f6c37d762aae44ab0807b2"}, |     {file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7221b83000ee01d674572eec1d1caa366eac109d1d32c14d7af9a4aaaf496563"}, | ||||||
|     {file = "cbor2-5.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc77cac985f7f7a20f2d8b1957d1e79393d7df823f61c7c6173d3a0011c1d770"}, |     {file = "cbor2-5.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9aca73b63bdc6561e1a0d38618e78b9c204c942260d51e663c92c4ba6c961684"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9e45d5aa8e484b4bf57240d8e7949389f1c9d4073758abb30954386321b55c9d"}, |     {file = "cbor2-5.6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:377cfe9d5560c682486faef6d856226abf8b2801d95fa29d4e5d75b1615eb091"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93b949a66bec40dd0ca87a6d026136fea2cf1660120f921199a47ac8027af253"}, |     {file = "cbor2-5.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fdc564ef2e9228bcd96ec8c6cdaa431a48ab03b3fb8326ead4b3f986330e5b9e"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d601ca92d917f769370a5e6c3ead62dca6451b2b603915e4fcf300083b9fcd"}, |     {file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d1c0021d9a1f673066de7c8941f71a59abb11909cc355892dda01e79a2b3045"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11876abd50b9f70d114fcdbb0b5a3249ccd7d321465f0350028fd6d2317e114"}, |     {file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fde9e704e96751e0729cc58b912d0e77c34387fb6bcceea0817069e8683df45"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fd77c558decdba2a2a7a463e6346d53781d2163bacf205f77b999f561ba4ac73"}, |     {file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:30e9ba8f4896726ca61869efacda50b6859aff92162ae5a0e192859664f36c81"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efb81920d80410b8e80a4a6a8b06ec9b766be0ae7f3029af8ae4b30914edcfa3"}, |     {file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a1e18e65ac71e04434ff5b58bde5c53f85b9c5bc92a3c0e2265089d3034f3"}, | ||||||
|     {file = "cbor2-5.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:4bb35f3b1ebd4b7b37628f0cd5c839f3008dec669194a2a4a33d91bab7f8663b"}, |     {file = "cbor2-5.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:94981277b4bf448a2754c1f34a9d0055a9d1c5a8d102c933ffe95c80f1085bae"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f41e4a439f642954ed728dc18915098b5f2ebec7029eaebe52c06c52b6a9a63a"}, |     {file = "cbor2-5.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f70db0ebcf005c25408e8d5cc4b9558c899f13a3e2f8281fa3d3be4894e0e821"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4eae4d56314f22920a28bf7affefdfc918646877ce3b16220dc6cf38a584aa41"}, |     {file = "cbor2-5.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:22c24fe9ef1696a84b8fd80ff66eb0e5234505d8b9a9711fc6db57bce10771f3"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559a0c1ec8dcedd6142b81727403e0f5a2e8f4c18e8bb3c548107ec39af4e9cb"}, |     {file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4a3420f80d6b942874d66eaad07658066370df994ddee4125b48b2cbc61ece"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537da7bfee97ee44a11b300c034c18e674af6a5dc4718a6fba141037f099c7ec"}, |     {file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b28d8ff0e726224a7429281700c28afe0e665f83f9ae79648cbae3f1a391cbf"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c99fd8bbc6bbf3bf4d6b2996594ae633b778b27b0531559487950762c4e1e3f"}, |     {file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c10ede9462458998f1b9c488e25fe3763aa2491119b7af472b72bf538d789e24"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ee46e6dbc8e2cf302a022fec513d57dba65e9d5ec495bcd1ad97a5dbdbab249"}, |     {file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea686dfb5e54d690e704ce04993bc8ca0052a7cd2d4b13dd333a41cca8a05a05"}, | ||||||
|     {file = "cbor2-5.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:67e2be461320197495fff55f250b111d4125a0a2d02e6256e41f8598adc3ad3f"}, |     {file = "cbor2-5.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:22996159b491d545ecfd489392d3c71e5d0afb9a202dfc0edc8b2cf413a58326"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4384a56afef0b908b61c8ea3cca3e257a316427ace3411308f51ee301b23adf9"}, |     {file = "cbor2-5.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9faa0712d414a88cc1244c78cd4b28fced44f1827dbd8c1649e3c40588aa670f"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8cc64acc606b7f2a4b673a1d6cde5a9cb1860a6ce27b353e269c9535efbd62c"}, |     {file = "cbor2-5.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6031a284d93fc953fc2a2918f261c4f5100905bd064ca3b46961643e7312a828"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50019fea3cb07fa9b2b53772a52b4243e87de232591570c4c272b3ebdb419493"}, |     {file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30c8a9a9df79f26e72d8d5fa51ef08eb250d9869a711bcf9539f1865916c983"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a18be0af9241883bc67a036c1f33e3f9956d31337ccd412194bf759bc1095e03"}, |     {file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bf7457fca23209e14dab8181dff82466a83b72e55b444dbbfe90fa67659492"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:60e7e0073291096605de27de3ce006148cf9a095199160439555f14f93d044d5"}, |     {file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc29c068687aa2e7778f63b653f1346065b858427a2555df4dc2191f4a0de8ce"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41f7501338228b27dac88c1197928cf8985f6fc775f59be89c6fdaddb4e69658"}, |     {file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42eaf0f768bd27afcb38135d5bfc361d3a157f1f5c7dddcd8d391f7fa43d9de8"}, | ||||||
|     {file = "cbor2-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c85ab7697252af2240e939707c935ea18081ccb580d4b5b9a94b04148ab2c32b"}, |     {file = "cbor2-5.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:8839b73befa010358477736680657b9d08c1ed935fd973decb1909712a41afdc"}, | ||||||
|     {file = "cbor2-5.5.1-py3-none-any.whl", hash = "sha256:dca639c8ff81b9f0c92faf97324adfdbfb5c2a5bb97f249606c6f5b94c77cc0d"}, |     {file = "cbor2-5.6.2-py3-none-any.whl", hash = "sha256:c0b53a65673550fde483724ff683753f49462d392d45d7b6576364b39e76e54c"}, | ||||||
|     {file = "cbor2-5.5.1.tar.gz", hash = "sha256:f9e192f461a9f8f6082df28c035b006d153904213dc8640bed8a72d72bbc9475"}, |     {file = "cbor2-5.6.2.tar.gz", hash = "sha256:b7513c2dea8868991fad7ef8899890ebcf8b199b9b4461c3c11d7ad3aef4820d"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.extras] | [package.extras] | ||||||
| @ -993,43 +993,43 @@ toml = ["tomli"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "cryptography" | name = "cryptography" | ||||||
| version = "42.0.0" | version = "42.0.4" | ||||||
| description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:c640b0ef54138fde761ec99a6c7dc4ce05e80420262c20fa239e694ca371d434"}, |     {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:678cfa0d1e72ef41d48993a7be75a76b0725d29b820ff3cfd606a5b2b33fda01"}, |     {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146e971e92a6dd042214b537a726c9750496128453146ab0ee8971a0299dc9bd"}, |     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87086eae86a700307b544625e3ba11cc600c3c0ef8ab97b0fda0705d6db3d4e3"}, |     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0a68bfcf57a6887818307600c3c0ebc3f62fbb6ccad2240aa21887cda1f8df1b"}, |     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5a217bca51f3b91971400890905a9323ad805838ca3fa1e202a01844f485ee87"}, |     {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ca20550bb590db16223eb9ccc5852335b48b8f597e2f6f0878bbfd9e7314eb17"}, |     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:33588310b5c886dfb87dba5f013b8d27df7ffd31dc753775342a1e5ab139e59d"}, |     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9515ea7f596c8092fdc9902627e51b23a75daa2c7815ed5aa8cf4f07469212ec"}, |     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:35cf6ed4c38f054478a9df14f03c1169bb14bd98f0b1705751079b25e1cb58bc"}, |     {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-win32.whl", hash = "sha256:8814722cffcfd1fbd91edd9f3451b88a8f26a5fd41b28c1c9193949d1c689dc4"}, |     {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"}, | ||||||
|     {file = "cryptography-42.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:a2a8d873667e4fd2f34aedab02ba500b824692c6542e017075a2efc38f60a4c0"}, |     {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8fedec73d590fd30c4e3f0d0f4bc961aeca8390c72f3eaa1a0874d180e868ddf"}, |     {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be41b0c7366e5549265adf2145135dca107718fa44b6e418dc7499cfff6b4689"}, |     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca482ea80626048975360c8e62be3ceb0f11803180b73163acd24bf014133a0"}, |     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c58115384bdcfe9c7f644c72f10f6f42bed7cf59f7b52fe1bf7ae0a622b3a139"}, |     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:56ce0c106d5c3fec1038c3cca3d55ac320a5be1b44bf15116732d0bc716979a2"}, |     {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:324721d93b998cb7367f1e6897370644751e5580ff9b370c0a50dc60a2003513"}, |     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:d97aae66b7de41cdf5b12087b5509e4e9805ed6f562406dfcf60e8481a9a28f8"}, |     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85f759ed59ffd1d0baad296e72780aa62ff8a71f94dc1ab340386a1207d0ea81"}, |     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:206aaf42e031b93f86ad60f9f5d9da1b09164f25488238ac1dc488334eb5e221"}, |     {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-win32.whl", hash = "sha256:74f18a4c8ca04134d2052a140322002fef535c99cdbc2a6afc18a8024d5c9d5b"}, |     {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"}, | ||||||
|     {file = "cryptography-42.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:14e4b909373bc5bf1095311fa0f7fcabf2d1a160ca13f1e9e467be1ac4cbdf94"}, |     {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"}, | ||||||
|     {file = "cryptography-42.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3005166a39b70c8b94455fdbe78d87a444da31ff70de3331cdec2c568cf25b7e"}, |     {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"}, | ||||||
|     {file = "cryptography-42.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:be14b31eb3a293fc6e6aa2807c8a3224c71426f7c4e3639ccf1a2f3ffd6df8c3"}, |     {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"}, | ||||||
|     {file = "cryptography-42.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bd7cf7a8d9f34cc67220f1195884151426ce616fdc8285df9054bfa10135925f"}, |     {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"}, | ||||||
|     {file = "cryptography-42.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c310767268d88803b653fffe6d6f2f17bb9d49ffceb8d70aed50ad45ea49ab08"}, |     {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"}, | ||||||
|     {file = "cryptography-42.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bdce70e562c69bb089523e75ef1d9625b7417c6297a76ac27b1b8b1eb51b7d0f"}, |     {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"}, | ||||||
|     {file = "cryptography-42.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e9326ca78111e4c645f7e49cbce4ed2f3f85e17b61a563328c85a5208cf34440"}, |     {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"}, | ||||||
|     {file = "cryptography-42.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:69fd009a325cad6fbfd5b04c711a4da563c6c4854fc4c9544bff3088387c77c0"}, |     {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"}, | ||||||
|     {file = "cryptography-42.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:988b738f56c665366b1e4bfd9045c3efae89ee366ca3839cd5af53eaa1401bce"}, |     {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"}, | ||||||
|     {file = "cryptography-42.0.0.tar.gz", hash = "sha256:6cf9b76d6e93c62114bd19485e5cb003115c134cf9ce91f8ac924c44f8c8c3f4"}, |     {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
|  | |||||||
| @ -113,7 +113,7 @@ filterwarnings = [ | |||||||
|  |  | ||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2023.10.7" | version = "2024.2.4" | ||||||
| description = "" | description = "" | ||||||
| authors = ["authentik Team <hello@goauthentik.io>"] | authors = ["authentik Team <hello@goauthentik.io>"] | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2023.10.7 |   version: 2024.2.4 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
|  | |||||||
| @ -6,3 +6,4 @@ dist | |||||||
| coverage | coverage | ||||||
| src/locale-codes.ts | src/locale-codes.ts | ||||||
| storybook-static/ | storybook-static/ | ||||||
|  | src/locales/** | ||||||
|  | |||||||
| @ -122,7 +122,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) { | |||||||
|                 ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], |                 ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], | ||||||
|                 ["/events/rules", msg("Notification Rules")], |                 ["/events/rules", msg("Notification Rules")], | ||||||
|                 ["/events/transports", msg("Notification Transports")]]], |                 ["/events/transports", msg("Notification Transports")]]], | ||||||
|             [null, msg("Customisation"), null, [ |             [null, msg("Customization"), null, [ | ||||||
|                 ["/policy/policies", msg("Policies")], |                 ["/policy/policies", msg("Policies")], | ||||||
|                 ["/core/property-mappings", msg("Property Mappings")], |                 ["/core/property-mappings", msg("Property Mappings")], | ||||||
|                 ["/blueprints/instances", msg("Blueprints")], |                 ["/blueprints/instances", msg("Blueprints")], | ||||||
|  | |||||||
| @ -22,25 +22,36 @@ import { AdminApi, Settings, SettingsRequest } from "@goauthentik/api"; | |||||||
|  |  | ||||||
| @customElement("ak-admin-settings-form") | @customElement("ak-admin-settings-form") | ||||||
| export class AdminSettingsForm extends Form<SettingsRequest> { | export class AdminSettingsForm extends Form<SettingsRequest> { | ||||||
|     @property({ attribute: false }) |     // | ||||||
|     set settings(value: Settings) { |     // Custom property accessors in Lit 2 require a manual call to requestUpdate(). See: | ||||||
|  |     // https://lit.dev/docs/v2/components/properties/#accessors-custom | ||||||
|  |     // | ||||||
|  |     set settings(value: Settings | undefined) { | ||||||
|         this._settings = value; |         this._settings = value; | ||||||
|  |         this.requestUpdate(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @property({ type: Object }) | ||||||
|  |     get settings() { | ||||||
|  |         return this._settings; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private _settings?: Settings; |     private _settings?: Settings; | ||||||
|  |  | ||||||
|  |     static get styles(): CSSResult[] { | ||||||
|  |         return super.styles.concat(PFList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     getSuccessMessage(): string { |     getSuccessMessage(): string { | ||||||
|         return msg("Successfully updated settings."); |         return msg("Successfully updated settings."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async send(data: SettingsRequest): Promise<Settings> { |     async send(data: SettingsRequest): Promise<Settings> { | ||||||
|         return new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({ |         const result = await new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({ | ||||||
|             settingsRequest: data, |             settingsRequest: data, | ||||||
|         }); |         }); | ||||||
|     } |         this.dispatchEvent(new CustomEvent("ak-admin-setting-changed")); | ||||||
|  |         return result; | ||||||
|     static get styles(): CSSResult[] { |  | ||||||
|         return super.styles.concat(PFList); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderForm(): TemplateResult { | ||||||
|  | |||||||
| @ -14,8 +14,8 @@ import "@goauthentik/elements/buttons/SpinnerButton"; | |||||||
| import "@goauthentik/elements/forms/ModalForm"; | import "@goauthentik/elements/forms/ModalForm"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, query, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| @ -32,7 +32,7 @@ import { AdminApi, Settings } from "@goauthentik/api"; | |||||||
|  |  | ||||||
| @customElement("ak-admin-settings") | @customElement("ak-admin-settings") | ||||||
| export class AdminSettingsPage extends AKElement { | export class AdminSettingsPage extends AKElement { | ||||||
|     static get styles(): CSSResult[] { |     static get styles() { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFButton, |             PFButton, | ||||||
| @ -46,41 +46,46 @@ export class AdminSettingsPage extends AKElement { | |||||||
|             PFBanner, |             PFBanner, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|     @property({ attribute: false }) |  | ||||||
|  |     @query("ak-admin-settings-form#form") | ||||||
|  |     form?: AdminSettingsForm; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|     settings?: Settings; |     settings?: Settings; | ||||||
|  |  | ||||||
|     loadSettings(): void { |     constructor() { | ||||||
|         new AdminApi(DEFAULT_CONFIG).adminSettingsRetrieve().then((settings) => { |         super(); | ||||||
|  |         AdminSettingsPage.fetchSettings().then((settings) => { | ||||||
|             this.settings = settings; |             this.settings = settings; | ||||||
|         }); |         }); | ||||||
|  |         this.save = this.save.bind(this); | ||||||
|  |         this.reset = this.reset.bind(this); | ||||||
|  |         this.addEventListener("ak-admin-setting-changed", this.handleUpdate.bind(this)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     firstUpdated(): void { |     static async fetchSettings() { | ||||||
|         this.loadSettings(); |         return await new AdminApi(DEFAULT_CONFIG).adminSettingsRetrieve(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async save(): Promise<void> { |     async handleUpdate() { | ||||||
|         const form = this.shadowRoot?.querySelector<AdminSettingsForm>("ak-admin-settings-form"); |         this.settings = await AdminSettingsPage.fetchSettings(); | ||||||
|         if (!form) { |     } | ||||||
|  |  | ||||||
|  |     async save() { | ||||||
|  |         if (!this.form) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         await form.submit(new Event("submit")); |         await this.form.submit(new Event("submit")); | ||||||
|         this.resetForm(); |         this.settings = await AdminSettingsPage.fetchSettings(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     resetForm(): void { |     async reset() { | ||||||
|         const form = this.shadowRoot?.querySelector<AdminSettingsForm>("ak-admin-settings-form"); |         this.form?.resetForm(); | ||||||
|         if (!form) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         this.loadSettings(); |  | ||||||
|         form.settings = this.settings!; |  | ||||||
|         form.resetForm(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render() { | ||||||
|         if (!this.settings) { |         if (!this.settings) { | ||||||
|             return html``; |             return nothing; | ||||||
|         } |         } | ||||||
|         return html` |         return html` | ||||||
|             <ak-page-header icon="fa fa-cog" header="" description=""> |             <ak-page-header icon="fa fa-cog" header="" description=""> | ||||||
| @ -93,18 +98,10 @@ export class AdminSettingsPage extends AKElement { | |||||||
|                         </ak-admin-settings-form> |                         </ak-admin-settings-form> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="pf-c-card__footer"> |                     <div class="pf-c-card__footer"> | ||||||
|                         <ak-spinner-button |                         <ak-spinner-button .callAction=${this.save} class="pf-m-primary" | ||||||
|                             .callAction=${async () => { |  | ||||||
|                                 await this.save(); |  | ||||||
|                             }} |  | ||||||
|                             class="pf-m-primary" |  | ||||||
|                             >${msg("Save")}</ak-spinner-button |                             >${msg("Save")}</ak-spinner-button | ||||||
|                         > |                         > | ||||||
|                         <ak-spinner-button |                         <ak-spinner-button .callAction=${this.reset} class="pf-m-secondary" | ||||||
|                             .callAction=${() => { |  | ||||||
|                                 this.resetForm(); |  | ||||||
|                             }} |  | ||||||
|                             class="pf-m-secondary" |  | ||||||
|                             >${msg("Cancel")}</ak-spinner-button |                             >${msg("Cancel")}</ak-spinner-button | ||||||
|                         > |                         > | ||||||
|                     </div> |                     </div> | ||||||
|  | |||||||
| @ -183,7 +183,6 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel { | |||||||
|                         <ak-multi-select |                         <ak-multi-select | ||||||
|                             label=${msg("AdditionalScopes")} |                             label=${msg("AdditionalScopes")} | ||||||
|                             name="propertyMappings" |                             name="propertyMappings" | ||||||
|                             required |  | ||||||
|                             .options=${scopePairs} |                             .options=${scopePairs} | ||||||
|                             .values=${scopeValues} |                             .values=${scopeValues} | ||||||
|                             .errorMessages=${errors?.propertyMappings ?? []} |                             .errorMessages=${errors?.propertyMappings ?? []} | ||||||
|  | |||||||
| @ -83,7 +83,6 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel { | |||||||
|                     <div slot="body" class="pf-c-form"> |                     <div slot="body" class="pf-c-form"> | ||||||
|                         <ak-form-element-horizontal |                         <ak-form-element-horizontal | ||||||
|                             label=${msg("Property mappings")} |                             label=${msg("Property mappings")} | ||||||
|                             ?required=${true} |  | ||||||
|                             name="propertyMappings" |                             name="propertyMappings" | ||||||
|                         > |                         > | ||||||
|                             <select class="pf-c-form-control" multiple> |                             <select class="pf-c-form-control" multiple> | ||||||
|  | |||||||
| @ -194,7 +194,6 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane | |||||||
|                         <ak-multi-select |                         <ak-multi-select | ||||||
|                             label=${msg("Property Mappings")} |                             label=${msg("Property Mappings")} | ||||||
|                             name="propertyMappings" |                             name="propertyMappings" | ||||||
|                             required |  | ||||||
|                             .options=${propertyPairs} |                             .options=${propertyPairs} | ||||||
|                             .values=${pmValues} |                             .values=${pmValues} | ||||||
|                             .richhelp=${html` <p class="pf-c-form__helper-text"> |                             .richhelp=${html` <p class="pf-c-form__helper-text"> | ||||||
|  | |||||||
| @ -123,7 +123,6 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel { | |||||||
|                     <div slot="body" class="pf-c-form"> |                     <div slot="body" class="pf-c-form"> | ||||||
|                         <ak-multi-select |                         <ak-multi-select | ||||||
|                             label=${msg("User Property Mappings")} |                             label=${msg("User Property Mappings")} | ||||||
|                             required |  | ||||||
|                             name="propertyMappings" |                             name="propertyMappings" | ||||||
|                             .options=${propertyPairs} |                             .options=${propertyPairs} | ||||||
|                             .values=${pmUserValues} |                             .values=${pmUserValues} | ||||||
| @ -136,7 +135,6 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel { | |||||||
|                         ></ak-multi-select> |                         ></ak-multi-select> | ||||||
|                         <ak-multi-select |                         <ak-multi-select | ||||||
|                             label=${msg("Group Property Mappings")} |                             label=${msg("Group Property Mappings")} | ||||||
|                             required |  | ||||||
|                             name="propertyMappingsGroup" |                             name="propertyMappingsGroup" | ||||||
|                             .options=${propertyPairs} |                             .options=${propertyPairs} | ||||||
|                             .values=${pmGroupValues} |                             .values=${pmGroupValues} | ||||||
|  | |||||||
| @ -125,6 +125,7 @@ export class RelatedGroupList extends Table<Group> { | |||||||
|             actionSubtext=${msg( |             actionSubtext=${msg( | ||||||
|                 str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`, |                 str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`, | ||||||
|             )} |             )} | ||||||
|  |             buttonLabel=${msg("Remove")} | ||||||
|             .objects=${this.selectedElements} |             .objects=${this.selectedElements} | ||||||
|             .delete=${(item: Group) => { |             .delete=${(item: Group) => { | ||||||
|                 if (!this.targetUser) return; |                 if (!this.targetUser) return; | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ import { customElement, property } from "lit/decorators.js"; | |||||||
|  |  | ||||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||||
|  |  | ||||||
| import { ConnectionToken, Endpoint, RACProvider, RacApi } from "@goauthentik/api"; | import { ConnectionToken, RACProvider, RacApi } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-rac-connection-token-list") | @customElement("ak-rac-connection-token-list") | ||||||
| export class ConnectionTokenListPage extends Table<ConnectionToken> { | export class ConnectionTokenListPage extends Table<ConnectionToken> { | ||||||
| @ -53,18 +53,18 @@ export class ConnectionTokenListPage extends Table<ConnectionToken> { | |||||||
|         return html`<ak-forms-delete-bulk |         return html`<ak-forms-delete-bulk | ||||||
|             objectLabel=${msg("Connection Token(s)")} |             objectLabel=${msg("Connection Token(s)")} | ||||||
|             .objects=${this.selectedElements} |             .objects=${this.selectedElements} | ||||||
|             .metadata=${(item: Endpoint) => { |             .metadata=${(item: ConnectionToken) => { | ||||||
|                 return [ |                 return [ | ||||||
|                     { key: msg("Name"), value: item.name }, |                     { key: msg("Endpoint"), value: item.endpointObj.name }, | ||||||
|                     { key: msg("Host"), value: item.host }, |                     { key: msg("User"), value: item.user.username }, | ||||||
|                 ]; |                 ]; | ||||||
|             }} |             }} | ||||||
|             .usedBy=${(item: Endpoint) => { |             .usedBy=${(item: ConnectionToken) => { | ||||||
|                 return new RacApi(DEFAULT_CONFIG).racConnectionTokensUsedByList({ |                 return new RacApi(DEFAULT_CONFIG).racConnectionTokensUsedByList({ | ||||||
|                     connectionTokenUuid: item.pk, |                     connectionTokenUuid: item.pk, | ||||||
|                 }); |                 }); | ||||||
|             }} |             }} | ||||||
|             .delete=${(item: Endpoint) => { |             .delete=${(item: ConnectionToken) => { | ||||||
|                 return new RacApi(DEFAULT_CONFIG).racConnectionTokensDestroy({ |                 return new RacApi(DEFAULT_CONFIG).racConnectionTokensDestroy({ | ||||||
|                     connectionTokenUuid: item.pk, |                     connectionTokenUuid: item.pk, | ||||||
|                 }); |                 }); | ||||||
|  | |||||||
| @ -123,11 +123,7 @@ export class EndpointForm extends ModelForm<Endpoint, string> { | |||||||
|                     )} |                     )} | ||||||
|                 </p> |                 </p> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|             <ak-form-element-horizontal |             <ak-form-element-horizontal label=${msg("Property mappings")} name="propertyMappings"> | ||||||
|                 label=${msg("Property mappings")} |  | ||||||
|                 ?required=${true} |  | ||||||
|                 name="propertyMappings" |  | ||||||
|             > |  | ||||||
|                 <select class="pf-c-form-control" multiple> |                 <select class="pf-c-form-control" multiple> | ||||||
|                     ${this.propertyMappings?.results.map((mapping) => { |                     ${this.propertyMappings?.results.map((mapping) => { | ||||||
|                         let selected = false; |                         let selected = false; | ||||||
|  | |||||||
| @ -135,7 +135,6 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> { | |||||||
|                 <div slot="body" class="pf-c-form"> |                 <div slot="body" class="pf-c-form"> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("Property mappings")} |                         label=${msg("Property mappings")} | ||||||
|                         ?required=${true} |  | ||||||
|                         name="propertyMappings" |                         name="propertyMappings" | ||||||
|                     > |                     > | ||||||
|                         <select class="pf-c-form-control" multiple> |                         <select class="pf-c-form-control" multiple> | ||||||
|  | |||||||
| @ -87,7 +87,11 @@ export class RACProviderViewPage extends AKElement { | |||||||
|             <section slot="page-overview" data-tab-title="${msg("Overview")}"> |             <section slot="page-overview" data-tab-title="${msg("Overview")}"> | ||||||
|                 ${this.renderTabOverview()} |                 ${this.renderTabOverview()} | ||||||
|             </section> |             </section> | ||||||
|             <section slot="page-connections" data-tab-title="${msg("Connections")}"> |             <section | ||||||
|  |                 slot="page-connections" | ||||||
|  |                 data-tab-title="${msg("Connections")}" | ||||||
|  |                 class="pf-c-page__main-section pf-m-no-padding-mobile" | ||||||
|  |             > | ||||||
|                 <div class="pf-c-card"> |                 <div class="pf-c-card"> | ||||||
|                     <div class="pf-c-card__body"> |                     <div class="pf-c-card__body"> | ||||||
|                         <ak-rac-connection-token-list |                         <ak-rac-connection-token-list | ||||||
|  | |||||||
| @ -191,7 +191,6 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> { | |||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("Property mappings")} |                         label=${msg("Property mappings")} | ||||||
|                         ?required=${true} |  | ||||||
|                         name="propertyMappings" |                         name="propertyMappings" | ||||||
|                     > |                     > | ||||||
|                         <select class="pf-c-form-control" multiple> |                         <select class="pf-c-form-control" multiple> | ||||||
|  | |||||||
| @ -151,7 +151,6 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> { | |||||||
|                 <div slot="body" class="pf-c-form"> |                 <div slot="body" class="pf-c-form"> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("User Property Mappings")} |                         label=${msg("User Property Mappings")} | ||||||
|                         ?required=${true} |  | ||||||
|                         name="propertyMappings" |                         name="propertyMappings" | ||||||
|                     > |                     > | ||||||
|                         <select class="pf-c-form-control" multiple> |                         <select class="pf-c-form-control" multiple> | ||||||
| @ -185,7 +184,6 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> { | |||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("Group Property Mappings")} |                         label=${msg("Group Property Mappings")} | ||||||
|                         ?required=${true} |  | ||||||
|                         name="propertyMappingsGroup" |                         name="propertyMappingsGroup" | ||||||
|                     > |                     > | ||||||
|                         <select class="pf-c-form-control" multiple> |                         <select class="pf-c-form-control" multiple> | ||||||
|  | |||||||
| @ -253,7 +253,6 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> { | |||||||
|                 <div slot="body" class="pf-c-form"> |                 <div slot="body" class="pf-c-form"> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("User Property Mappings")} |                         label=${msg("User Property Mappings")} | ||||||
|                         ?required=${true} |  | ||||||
|                         name="propertyMappings" |                         name="propertyMappings" | ||||||
|                     > |                     > | ||||||
|                         <select class="pf-c-form-control" multiple> |                         <select class="pf-c-form-control" multiple> | ||||||
| @ -292,7 +291,6 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> { | |||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                     <ak-form-element-horizontal |                     <ak-form-element-horizontal | ||||||
|                         label=${msg("Group Property Mappings")} |                         label=${msg("Group Property Mappings")} | ||||||
|                         ?required=${true} |  | ||||||
|                         name="propertyMappingsGroup" |                         name="propertyMappingsGroup" | ||||||
|                     > |                     > | ||||||
|                         <select class="pf-c-form-control" multiple> |                         <select class="pf-c-form-control" multiple> | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2023.10.7"; | export const VERSION = "2024.2.4"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { PFSize } from "@goauthentik/elements/Spinner"; | import { PFSize } from "@goauthentik/elements/Spinner"; | ||||||
|  |  | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; | import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; | ||||||
| @ -23,7 +23,17 @@ export class EmptyState extends AKElement { | |||||||
|     header = ""; |     header = ""; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase, PFEmptyState, PFTitle]; |         return [ | ||||||
|  |             PFBase, | ||||||
|  |             PFEmptyState, | ||||||
|  |             PFTitle, | ||||||
|  |             css` | ||||||
|  |                 i.pf-c-empty-state__icon { | ||||||
|  |                     height: var(--pf-global--icon--FontSize--2xl); | ||||||
|  |                     line-height: var(--pf-global--icon--FontSize--2xl); | ||||||
|  |                 } | ||||||
|  |             `, | ||||||
|  |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|  | |||||||
| @ -131,6 +131,9 @@ export class DeleteBulkForm<T> extends ModalButton { | |||||||
|     @property() |     @property() | ||||||
|     actionSubtext?: string; |     actionSubtext?: string; | ||||||
|  |  | ||||||
|  |     @property() | ||||||
|  |     buttonLabel = msg("Delete"); | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     metadata: (item: T) => BulkDeleteMetadata = (item: T) => { |     metadata: (item: T) => BulkDeleteMetadata = (item: T) => { | ||||||
|         const rec = item as Record<string, unknown>; |         const rec = item as Record<string, unknown>; | ||||||
| @ -222,7 +225,7 @@ export class DeleteBulkForm<T> extends ModalButton { | |||||||
|                     }} |                     }} | ||||||
|                     class="pf-m-danger" |                     class="pf-m-danger" | ||||||
|                 > |                 > | ||||||
|                     ${msg("Delete")} </ak-spinner-button |                     ${this.buttonLabel} </ak-spinner-button | ||||||
|                 >  |                 >  | ||||||
|                 <ak-spinner-button |                 <ak-spinner-button | ||||||
|                     .callAction=${async () => { |                     .callAction=${async () => { | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ import "@goauthentik/flow/sources/apple/AppleLoginInit"; | |||||||
| import "@goauthentik/flow/sources/plex/PlexLoginInit"; | import "@goauthentik/flow/sources/plex/PlexLoginInit"; | ||||||
| import "@goauthentik/flow/stages/FlowErrorStage"; | import "@goauthentik/flow/stages/FlowErrorStage"; | ||||||
| import "@goauthentik/flow/stages/RedirectStage"; | import "@goauthentik/flow/stages/RedirectStage"; | ||||||
| import { StageHost } from "@goauthentik/flow/stages/base"; | import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | ||||||
| @ -189,12 +189,17 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; |         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async submit(payload?: FlowChallengeResponseRequest): Promise<boolean> { |     async submit( | ||||||
|  |         payload?: FlowChallengeResponseRequest, | ||||||
|  |         options?: SubmitOptions, | ||||||
|  |     ): Promise<boolean> { | ||||||
|         if (!payload) return Promise.reject(); |         if (!payload) return Promise.reject(); | ||||||
|         if (!this.challenge) return Promise.reject(); |         if (!this.challenge) return Promise.reject(); | ||||||
|         // @ts-ignore |         // @ts-expect-error | ||||||
|         payload.component = this.challenge.component; |         payload.component = this.challenge.component; | ||||||
|         this.loading = true; |         if (!options?.invisible) { | ||||||
|  |             this.loading = true; | ||||||
|  |         } | ||||||
|         try { |         try { | ||||||
|             const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ |             const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ | ||||||
|                 flowSlug: this.flowSlug, |                 flowSlug: this.flowSlug, | ||||||
|  | |||||||
| @ -40,6 +40,7 @@ export class AuthenticatorStaticStage extends BaseStage< | |||||||
|                     columns: 2; |                     columns: 2; | ||||||
|                     -webkit-columns: 2; |                     -webkit-columns: 2; | ||||||
|                     -moz-columns: 2; |                     -moz-columns: 2; | ||||||
|  |                     column-width: 1em; | ||||||
|                     margin-left: var(--pf-global--spacer--xs); |                     margin-left: var(--pf-global--spacer--xs); | ||||||
|                 } |                 } | ||||||
|                 ul li { |                 ul li { | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	