Compare commits
	
		
			288 Commits
		
	
	
		
			version/20
			...
			web/bug/fi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1c9e4652f9 | |||
| 079ed7bf4c | |||
| 524f9ff18e | |||
| 2f3bd625c6 | |||
| b0e471fbd4 | |||
| 982aee44bb | |||
| 09fe224895 | |||
| 68fbec404d | |||
| dbc05b88ad | |||
| da1b7e4248 | |||
| 5453710c39 | |||
| 2e79ad5adf | |||
| 31d908f3e2 | |||
| 7cf8991cfc | |||
| 17d666670a | |||
| 430278712f | |||
| 2aad79b5db | |||
| 0fcf1ebf94 | |||
| 4a5058dd15 | |||
| 4af2be894b | |||
| 975b6e53a6 | |||
| ed7c1b9b3e | |||
| e71e875056 | |||
| 384ca87aad | |||
| 5ce8d65546 | |||
| ba28e6de41 | |||
| 97a36b6c4e | |||
| 6088e59b9f | |||
| cdeed5e13b | |||
| 6d291d378e | |||
| 49b79c477a | |||
| 02ffac791c | |||
| 95c2a24bd2 | |||
| 7a0bf613b4 | |||
| 705f096e3f | |||
| 035648f0de | |||
| ff53bccc0f | |||
| 4fbc13ad81 | |||
| 48f3abfd91 | |||
| 1168856d33 | |||
| 6bfa0f7d7b | |||
| ec90b668a1 | |||
| bb4d7adfd1 | |||
| 5655b9c2bf | |||
| b7b9bd4fbe | |||
| 51065b415e | |||
| c958df9e7c | |||
| 004fb105ed | |||
| 0fde303749 | |||
| 896ee8925f | |||
| dd7ce456b1 | |||
| e941981a3a | |||
| 4fd8b3c532 | |||
| 91a315d864 | |||
| 05b990e378 | |||
| 7964c0cff7 | |||
| 0cae4a2de2 | |||
| c6b3b877ee | |||
| 2909f2787a | |||
| 49a10efd6b | |||
| aca20fc3af | |||
| c80d881400 | |||
| 27380e9903 | |||
| ab4a125613 | |||
| 4fed92127d | |||
| 4abef6d7b9 | |||
| ffcd146b2a | |||
| 6d1c48bb8b | |||
| ba6ddcb655 | |||
| d5581fde17 | |||
| 33b3e9bbf9 | |||
| 421873b15f | |||
| d351edc3d4 | |||
| 2ce1c3f724 | |||
| 09ad69b0be | |||
| b144d0cf05 | |||
| f386856aa6 | |||
| 849951e747 | |||
| 9b7aa99713 | |||
| a46189ff21 | |||
| 2ce6ede0f0 | |||
| 7a5850b1e8 | |||
| f9cc835eba | |||
| f5ed14d9a8 | |||
| 393584cd71 | |||
| 9c42df7191 | |||
| bad0203e94 | |||
| ad3820c11c | |||
| 20f555ebb6 | |||
| c39808f398 | |||
| 113c87baef | |||
| a96639ba06 | |||
| 32a61f7cc9 | |||
| 31175862fe | |||
| 11fef40eeb | |||
| e80d76cca3 | |||
| 7f075eb103 | |||
| 70b5a214f2 | |||
| c4d6388b1e | |||
| b27bf1ec56 | |||
| 37d0de767e | |||
| 5dd9bd1e44 | |||
| 4e56dda1b8 | |||
| e130f16809 | |||
| cdf5acd9ec | |||
| b4ec306f93 | |||
| bb56c4fa49 | |||
| 6b94fecf7b | |||
| 5822653155 | |||
| f0ec7511cf | |||
| e4f711dab1 | |||
| 1892112b6f | |||
| e3aa35970e | |||
| a343848c7b | |||
| 02fba59985 | |||
| fd7d276d9a | |||
| e87c946403 | |||
| 1182009dac | |||
| 02d8f82a5e | |||
| ebf39139cb | |||
| 480c0284b5 | |||
| 00547bcf3f | |||
| 0a91f2fe3f | |||
| 48ea4a9fa3 | |||
| 5b192c51f9 | |||
| 3644ccbe02 | |||
| 0adcb0d081 | |||
| d5b717efce | |||
| 8661b764c1 | |||
| 4a54a127c1 | |||
| cbf90b7422 | |||
| f4dfa241b7 | |||
| 41b310f62d | |||
| aa1277f700 | |||
| 171d0f55cb | |||
| 18a43455f1 | |||
| 0e41df42b7 | |||
| a3113d3b26 | |||
| edef110dca | |||
| 0bcc778dda | |||
| c76283ffae | |||
| 408fa926b3 | |||
| dbff42a338 | |||
| 20552dc899 | |||
| 65166e8a2a | |||
| 38236ed95d | |||
| 8958b225b4 | |||
| 0145b74103 | |||
| 220f540c50 | |||
| 3ab9747ebe | |||
| 52d2514461 | |||
| c12958aebd | |||
| 7b66850d8e | |||
| 4281b6be2e | |||
| 9df63af6f8 | |||
| 8a31acbe91 | |||
| f207ce64f8 | |||
| 8a9d4c1763 | |||
| c6383981ef | |||
| a0cf9b246d | |||
| 5b98368555 | |||
| f29d38116c | |||
| 2bc1607c0f | |||
| d5a087c0ef | |||
| b4df730990 | |||
| af8e33edcf | |||
| f9f93d50c7 | |||
| 8af8061ca5 | |||
| af5a0d3311 | |||
| 3308c0001a | |||
| 52cd4a909c | |||
| ed38372a63 | |||
| 81f22dbce8 | |||
| 0e9b8b3a95 | |||
| 8e902f9d09 | |||
| 8c19385803 | |||
| ad21b41053 | |||
| e99b119c88 | |||
| 1ba1acd82b | |||
| de33eae5f1 | |||
| 9d8ba2825b | |||
| f3b9fca890 | |||
| b0e5d59c31 | |||
| 378daba74e | |||
| e1b969446d | |||
| 88928d7f72 | |||
| b8ae028d4d | |||
| c90ecf4ebe | |||
| c3b6d8ea70 | |||
| ebc258efe0 | |||
| 1eb69ceb38 | |||
| dee7e12669 | |||
| 205cc084ac | |||
| 5be49a8e80 | |||
| ab76155e7a | |||
| a5fc85c2d8 | |||
| 2bc8e9bae6 | |||
| 6dabd83a99 | |||
| f790cd689c | |||
| 3696faf299 | |||
| fcc5c57589 | |||
| 710809a9ef | |||
| 266e5a2cbd | |||
| ae079ea490 | |||
| 6123f6bf1e | |||
| 984466f77f | |||
| 31f5e19c21 | |||
| 4670f12ba7 | |||
| ea74ad4090 | |||
| a8b33b25e8 | |||
| 8886532ed6 | |||
| 4b63e11d29 | |||
| 2cceec66fa | |||
| 30fcdbf05f | |||
| 31ad841aed | |||
| c4f149dd46 | |||
| 89245123dc | |||
| 55d9da18d5 | |||
| 4b80d860b6 | |||
| 90f5c4c757 | |||
| 67c90d7ec5 | |||
| eed3ffd0ee | |||
| 7ff42c34fb | |||
| b2873fad10 | |||
| 441b6ca6b0 | |||
| 5f261bed96 | |||
| 4d51671f88 | |||
| 1c570b2502 | |||
| 78e4370b98 | |||
| 9a26111f1c | |||
| 02ae099bdf | |||
| 05fe4e5e7b | |||
| 24c289fdd1 | |||
| cd8de3e526 | |||
| 243bd03785 | |||
| e02cf0b3bd | |||
| 3018735579 | |||
| 6a956d149a | |||
| 3fd1bc6673 | |||
| 6bc4877702 | |||
| 9592b42501 | |||
| 517a5bc689 | |||
| a19e350ca6 | |||
| 88577145fb | |||
| bc93df1e29 | |||
| 9d5eb54504 | |||
| d95c433027 | |||
| 7416c90efb | |||
| 34a073b0f7 | |||
| 86eb112a03 | |||
| e48d001ea6 | |||
| 2070372f03 | |||
| bea4679192 | |||
| 32d1488a56 | |||
| 1003c79d8c | |||
| a05ed7e237 | |||
| 087d4f6a48 | |||
| 141cfe75d8 | |||
| a3a13d265b | |||
| 9c45ec1918 | |||
| 59fa449abe | |||
| f83c84b04d | |||
| 835a4097eb | |||
| da43df44b2 | |||
| d03b14aac4 | |||
| 62d990e91b | |||
| 6c8cfc9ef7 | |||
| e93d8b1646 | |||
| 6faa250574 | |||
| c8e4b187b8 | |||
| 98acca896a | |||
| 7dc4b70ee1 | |||
| 8f55d3fc07 | |||
| 17fb90e0af | |||
| 870ed99097 | |||
| 1c5af88ea9 | |||
| 461856d067 | |||
| cdbf448769 | |||
| 0fcac0e165 | |||
| 40e857fdb3 | |||
| 7d86593d05 | |||
| c95ce0a5d4 | |||
| a7b31ce6de | |||
| b5f4303fbd | |||
| 29f04ea801 | |||
| 7141702c9e | |||
| 0f30d135b6 | |||
| 73f326b21b | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2024.8.6 | current_version = 2024.8.3 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @ -44,9 +44,11 @@ updates: | |||||||
|           - "babel-*" |           - "babel-*" | ||||||
|       eslint: |       eslint: | ||||||
|         patterns: |         patterns: | ||||||
|  |           - "@eslint/*" | ||||||
|           - "@typescript-eslint/*" |           - "@typescript-eslint/*" | ||||||
|           - "eslint" |  | ||||||
|           - "eslint-*" |           - "eslint-*" | ||||||
|  |           - "eslint" | ||||||
|  |           - "typescript-eslint" | ||||||
|       storybook: |       storybook: | ||||||
|         patterns: |         patterns: | ||||||
|           - "@storybook/*" |           - "@storybook/*" | ||||||
| @ -54,10 +56,12 @@ updates: | |||||||
|       esbuild: |       esbuild: | ||||||
|         patterns: |         patterns: | ||||||
|           - "@esbuild/*" |           - "@esbuild/*" | ||||||
|  |           - "esbuild*" | ||||||
|       rollup: |       rollup: | ||||||
|         patterns: |         patterns: | ||||||
|           - "@rollup/*" |           - "@rollup/*" | ||||||
|           - "rollup-*" |           - "rollup-*" | ||||||
|  |           - "rollup*" | ||||||
|       swc: |       swc: | ||||||
|         patterns: |         patterns: | ||||||
|           - "@swc/*" |           - "@swc/*" | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/api-ts-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/api-ts-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -40,7 +40,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'` |           export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'` | ||||||
|           npm i @goauthentik/api@$VERSION |           npm i @goauthentik/api@$VERSION | ||||||
|       - uses: peter-evans/create-pull-request@v6 |       - uses: peter-evans/create-pull-request@v7 | ||||||
|         id: cpr |         id: cpr | ||||||
|         with: |         with: | ||||||
|           token: ${{ steps.generate_token.outputs.token }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -120,6 +120,12 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           flags: unit |           flags: unit | ||||||
|           token: ${{ secrets.CODECOV_TOKEN }} |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|  |       - if: ${{ !cancelled() }} | ||||||
|  |         uses: codecov/test-results-action@v1 | ||||||
|  |         with: | ||||||
|  |           flags: unit | ||||||
|  |           file: unittest.xml | ||||||
|  |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|   test-integration: |   test-integration: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     timeout-minutes: 30 |     timeout-minutes: 30 | ||||||
| @ -138,6 +144,12 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           flags: integration |           flags: integration | ||||||
|           token: ${{ secrets.CODECOV_TOKEN }} |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|  |       - if: ${{ !cancelled() }} | ||||||
|  |         uses: codecov/test-results-action@v1 | ||||||
|  |         with: | ||||||
|  |           flags: integration | ||||||
|  |           file: unittest.xml | ||||||
|  |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|   test-e2e: |   test-e2e: | ||||||
|     name: test-e2e (${{ matrix.job.name }}) |     name: test-e2e (${{ matrix.job.name }}) | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| @ -190,6 +202,12 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           flags: e2e |           flags: e2e | ||||||
|           token: ${{ secrets.CODECOV_TOKEN }} |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|  |       - if: ${{ !cancelled() }} | ||||||
|  |         uses: codecov/test-results-action@v1 | ||||||
|  |         with: | ||||||
|  |           flags: e2e | ||||||
|  |           file: unittest.xml | ||||||
|  |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|   ci-core-mark: |   ci-core-mark: | ||||||
|     needs: |     needs: | ||||||
|       - lint |       - lint | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							| @ -45,7 +45,6 @@ jobs: | |||||||
|       - working-directory: ${{ matrix.project }}/ |       - working-directory: ${{ matrix.project }}/ | ||||||
|         run: | |         run: | | ||||||
|           npm ci |           npm ci | ||||||
|           ${{ matrix.extra_setup }} |  | ||||||
|       - name: Generate API |       - name: Generate API | ||||||
|         run: make gen-client-ts |         run: make gen-client-ts | ||||||
|       - name: Lint |       - name: Lint | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ jobs: | |||||||
|       - name: Setup authentik env |       - name: Setup authentik env | ||||||
|         uses: ./.github/actions/setup |         uses: ./.github/actions/setup | ||||||
|       - run: poetry run ak update_webauthn_mds |       - run: poetry run ak update_webauthn_mds | ||||||
|       - uses: peter-evans/create-pull-request@v6 |       - uses: peter-evans/create-pull-request@v7 | ||||||
|         id: cpr |         id: cpr | ||||||
|         with: |         with: | ||||||
|           token: ${{ steps.generate_token.outputs.token }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/image-compress.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/image-compress.yml
									
									
									
									
										vendored
									
									
								
							| @ -42,7 +42,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           githubToken: ${{ steps.generate_token.outputs.token }} |           githubToken: ${{ steps.generate_token.outputs.token }} | ||||||
|           compressOnly: ${{ github.event_name != 'pull_request' }} |           compressOnly: ${{ github.event_name != 'pull_request' }} | ||||||
|       - uses: peter-evans/create-pull-request@v6 |       - uses: peter-evans/create-pull-request@v7 | ||||||
|         if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}" |         if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}" | ||||||
|         id: cpr |         id: cpr | ||||||
|         with: |         with: | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ jobs: | |||||||
|           poetry run ak compilemessages |           poetry run ak compilemessages | ||||||
|           make web-check-compile |           make web-check-compile | ||||||
|       - name: Create Pull Request |       - name: Create Pull Request | ||||||
|         uses: peter-evans/create-pull-request@v6 |         uses: peter-evans/create-pull-request@v7 | ||||||
|         with: |         with: | ||||||
|           token: ${{ steps.generate_token.outputs.token }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|           branch: extract-compile-backend-translation |           branch: extract-compile-backend-translation | ||||||
|  | |||||||
| @ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | |||||||
|     /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" |     /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" | ||||||
|  |  | ||||||
| # Stage 5: Python dependencies | # Stage 5: Python dependencies | ||||||
| FROM ghcr.io/goauthentik/fips-python:3.12.5-slim-bookworm-fips-full AS python-deps | FROM ghcr.io/goauthentik/fips-python:3.12.6-slim-bookworm-fips-full AS python-deps | ||||||
|  |  | ||||||
| ARG TARGETARCH | ARG TARGETARCH | ||||||
| ARG TARGETVARIANT | ARG TARGETVARIANT | ||||||
| @ -124,7 +124,7 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \ | |||||||
|     pip install --force-reinstall /wheels/*" |     pip install --force-reinstall /wheels/*" | ||||||
|  |  | ||||||
| # Stage 6: Run | # Stage 6: Run | ||||||
| FROM ghcr.io/goauthentik/fips-python:3.12.5-slim-bookworm-fips-full AS final-image | FROM ghcr.io/goauthentik/fips-python:3.12.6-slim-bookworm-fips-full AS final-image | ||||||
|  |  | ||||||
| ARG VERSION | ARG VERSION | ||||||
| ARG GIT_BUILD_HASH | ARG GIT_BUILD_HASH | ||||||
|  | |||||||
| @ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni | |||||||
|  |  | ||||||
| | Version  | Supported | | | Version  | Supported | | ||||||
| | -------- | --------- | | | -------- | --------- | | ||||||
| | 2024.4.x | ✅        | |  | ||||||
| | 2024.6.x | ✅        | | | 2024.6.x | ✅        | | ||||||
|  | | 2024.8.x | ✅        | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| __version__ = "2024.8.6" | __version__ = "2024.8.3" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,10 +1,8 @@ | |||||||
| """authentik admin tasks""" | """authentik admin tasks""" | ||||||
|  |  | ||||||
| import re |  | ||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.core.validators import URLValidator |  | ||||||
| from django.db import DatabaseError, InternalError, ProgrammingError | from django.db import DatabaseError, InternalError, ProgrammingError | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
| from packaging.version import parse | from packaging.version import parse | ||||||
| from requests import RequestException | from requests import RequestException | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| @ -21,8 +19,6 @@ LOGGER = get_logger() | |||||||
| VERSION_NULL = "0.0.0" | VERSION_NULL = "0.0.0" | ||||||
| VERSION_CACHE_KEY = "authentik_latest_version" | VERSION_CACHE_KEY = "authentik_latest_version" | ||||||
| VERSION_CACHE_TIMEOUT = 8 * 60 * 60  # 8 hours | VERSION_CACHE_TIMEOUT = 8 * 60 * 60  # 8 hours | ||||||
| # Chop of the first ^ because we want to search the entire string |  | ||||||
| URL_FINDER = URLValidator.regex.pattern[1:] |  | ||||||
| LOCAL_VERSION = parse(__version__) | LOCAL_VERSION = parse(__version__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -78,10 +74,16 @@ def update_latest_version(self: SystemTask): | |||||||
|                 context__new_version=upstream_version, |                 context__new_version=upstream_version, | ||||||
|             ).exists(): |             ).exists(): | ||||||
|                 return |                 return | ||||||
|             event_dict = {"new_version": upstream_version} |             Event.new( | ||||||
|             if match := re.search(URL_FINDER, data.get("stable", {}).get("changelog", "")): |                 EventAction.UPDATE_AVAILABLE, | ||||||
|                 event_dict["message"] = f"Changelog: {match.group()}" |                 message=_( | ||||||
|             Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save() |                     "New version {version} available!".format( | ||||||
|  |                         version=upstream_version, | ||||||
|  |                     ) | ||||||
|  |                 ), | ||||||
|  |                 new_version=upstream_version, | ||||||
|  |                 changelog=data.get("stable", {}).get("changelog_url"), | ||||||
|  |             ).save() | ||||||
|     except (RequestException, IndexError) as exc: |     except (RequestException, IndexError) as exc: | ||||||
|         cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT) |         cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT) | ||||||
|         self.set_error(exc) |         self.set_error(exc) | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ RESPONSE_VALID = { | |||||||
|     "stable": { |     "stable": { | ||||||
|         "version": "99999999.9999999", |         "version": "99999999.9999999", | ||||||
|         "changelog": "See https://goauthentik.io/test", |         "changelog": "See https://goauthentik.io/test", | ||||||
|  |         "changelog_url": "https://goauthentik.io/test", | ||||||
|         "reason": "bugfix", |         "reason": "bugfix", | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
| @ -35,7 +36,7 @@ class TestAdminTasks(TestCase): | |||||||
|                 Event.objects.filter( |                 Event.objects.filter( | ||||||
|                     action=EventAction.UPDATE_AVAILABLE, |                     action=EventAction.UPDATE_AVAILABLE, | ||||||
|                     context__new_version="99999999.9999999", |                     context__new_version="99999999.9999999", | ||||||
|                     context__message="Changelog: https://goauthentik.io/test", |                     context__message="New version 99999999.9999999 available!", | ||||||
|                 ).exists() |                 ).exists() | ||||||
|             ) |             ) | ||||||
|             # test that a consecutive check doesn't create a duplicate event |             # test that a consecutive check doesn't create a duplicate event | ||||||
| @ -45,7 +46,7 @@ class TestAdminTasks(TestCase): | |||||||
|                     Event.objects.filter( |                     Event.objects.filter( | ||||||
|                         action=EventAction.UPDATE_AVAILABLE, |                         action=EventAction.UPDATE_AVAILABLE, | ||||||
|                         context__new_version="99999999.9999999", |                         context__new_version="99999999.9999999", | ||||||
|                         context__message="Changelog: https://goauthentik.io/test", |                         context__message="New version 99999999.9999999 available!", | ||||||
|                     ) |                     ) | ||||||
|                 ), |                 ), | ||||||
|                 1, |                 1, | ||||||
|  | |||||||
| @ -51,11 +51,9 @@ class BlueprintInstanceSerializer(ModelSerializer): | |||||||
|         context = self.instance.context if self.instance else {} |         context = self.instance.context if self.instance else {} | ||||||
|         valid, logs = Importer.from_string(content, context).validate() |         valid, logs = Importer.from_string(content, context).validate() | ||||||
|         if not valid: |         if not valid: | ||||||
|  |             text_logs = "\n".join([x["event"] for x in logs]) | ||||||
|             raise ValidationError( |             raise ValidationError( | ||||||
|                 [ |                 _("Failed to validate blueprint: {logs}".format_map({"logs": text_logs})) | ||||||
|                     _("Failed to validate blueprint"), |  | ||||||
|                     *[f"- {x.event}" for x in logs], |  | ||||||
|                 ] |  | ||||||
|             ) |             ) | ||||||
|         return content |         return content | ||||||
|  |  | ||||||
|  | |||||||
| @ -78,5 +78,5 @@ class TestBlueprintsV1API(APITestCase): | |||||||
|         self.assertEqual(res.status_code, 400) |         self.assertEqual(res.status_code, 400) | ||||||
|         self.assertJSONEqual( |         self.assertJSONEqual( | ||||||
|             res.content.decode(), |             res.content.decode(), | ||||||
|             {"content": ["Failed to validate blueprint", "- Invalid blueprint version"]}, |             {"content": ["Failed to validate blueprint: Invalid blueprint version"]}, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -429,7 +429,7 @@ class Importer: | |||||||
|         orig_import = deepcopy(self._import) |         orig_import = deepcopy(self._import) | ||||||
|         if self._import.version != 1: |         if self._import.version != 1: | ||||||
|             self.logger.warning("Invalid blueprint version") |             self.logger.warning("Invalid blueprint version") | ||||||
|             return False, [LogEvent("Invalid blueprint version", log_level="warning", logger=None)] |             return False, [{"event": "Invalid blueprint version"}] | ||||||
|         with ( |         with ( | ||||||
|             transaction_rollback(), |             transaction_rollback(), | ||||||
|             capture_logs() as logs, |             capture_logs() as logs, | ||||||
|  | |||||||
| @ -679,10 +679,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|             LOGGER.debug("User attempted to impersonate", user=request.user) |             LOGGER.debug("User attempted to impersonate", user=request.user) | ||||||
|             return Response(status=401) |             return Response(status=401) | ||||||
|         user_to_be = self.get_object() |         user_to_be = self.get_object() | ||||||
|         # Check both object-level perms and global perms |         if not request.user.has_perm("impersonate", user_to_be): | ||||||
|         if not request.user.has_perm( |  | ||||||
|             "authentik_core.impersonate", user_to_be |  | ||||||
|         ) and not request.user.has_perm("authentik_core.impersonate"): |  | ||||||
|             LOGGER.debug("User attempted to impersonate without permissions", user=request.user) |             LOGGER.debug("User attempted to impersonate without permissions", user=request.user) | ||||||
|             return Response(status=401) |             return Response(status=401) | ||||||
|         if user_to_be.pk == self.request.user.pk: |         if user_to_be.pk == self.request.user.pk: | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow | |||||||
| 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 | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode | from authentik.providers.oauth2.models import OAuth2Provider | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
| from authentik.providers.saml.models import SAMLProvider | from authentik.providers.saml.models import SAMLProvider | ||||||
|  |  | ||||||
| @ -24,7 +24,7 @@ class TestApplicationsAPI(APITestCase): | |||||||
|         self.user = create_test_admin_user() |         self.user = create_test_admin_user() | ||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")], |             redirect_uris="http://some-other-domain", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|         ) |         ) | ||||||
|         self.allowed: Application = Application.objects.create( |         self.allowed: Application = Application.objects.create( | ||||||
|  | |||||||
| @ -44,26 +44,6 @@ class TestImpersonation(APITestCase): | |||||||
|         self.assertEqual(response_body["user"]["username"], self.user.username) |         self.assertEqual(response_body["user"]["username"], self.user.username) | ||||||
|         self.assertNotIn("original", response_body) |         self.assertNotIn("original", response_body) | ||||||
|  |  | ||||||
|     def test_impersonate_global(self): |  | ||||||
|         """Test impersonation with global permissions""" |  | ||||||
|         new_user = create_test_user() |  | ||||||
|         assign_perm("authentik_core.impersonate", new_user) |  | ||||||
|         assign_perm("authentik_core.view_user", new_user) |  | ||||||
|         self.client.force_login(new_user) |  | ||||||
|  |  | ||||||
|         response = self.client.post( |  | ||||||
|             reverse( |  | ||||||
|                 "authentik_api:user-impersonate", |  | ||||||
|                 kwargs={"pk": self.other_user.pk}, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(response.status_code, 201) |  | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_api:user-me")) |  | ||||||
|         response_body = loads(response.content.decode()) |  | ||||||
|         self.assertEqual(response_body["user"]["username"], self.other_user.username) |  | ||||||
|         self.assertEqual(response_body["original"]["username"], new_user.username) |  | ||||||
|  |  | ||||||
|     def test_impersonate_scoped(self): |     def test_impersonate_scoped(self): | ||||||
|         """Test impersonation with scoped permissions""" |         """Test impersonation with scoped permissions""" | ||||||
|         new_user = create_test_user() |         new_user = create_test_user() | ||||||
|  | |||||||
| @ -31,7 +31,6 @@ class TestTransactionalApplicationsAPI(APITestCase): | |||||||
|                 "provider": { |                 "provider": { | ||||||
|                     "name": uid, |                     "name": uid, | ||||||
|                     "authorization_flow": str(authorization_flow.pk), |                     "authorization_flow": str(authorization_flow.pk), | ||||||
|                     "redirect_uris": [], |  | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
| @ -57,7 +56,6 @@ class TestTransactionalApplicationsAPI(APITestCase): | |||||||
|                 "provider": { |                 "provider": { | ||||||
|                     "name": uid, |                     "name": uid, | ||||||
|                     "authorization_flow": "", |                     "authorization_flow": "", | ||||||
|                     "redirect_uris": [], |  | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ from authentik.crypto.models import CertificateKeyPair | |||||||
| from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery | from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode | from authentik.providers.oauth2.models import OAuth2Provider | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestCrypto(APITestCase): | class TestCrypto(APITestCase): | ||||||
| @ -263,7 +263,7 @@ class TestCrypto(APITestCase): | |||||||
|             client_id="test", |             client_id="test", | ||||||
|             client_secret=generate_key(), |             client_secret=generate_key(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], |             redirect_uris="http://localhost", | ||||||
|             signing_key=keypair, |             signing_key=keypair, | ||||||
|         ) |         ) | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
| @ -295,7 +295,7 @@ class TestCrypto(APITestCase): | |||||||
|             client_id="test", |             client_id="test", | ||||||
|             client_secret=generate_key(), |             client_secret=generate_key(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], |             redirect_uris="http://localhost", | ||||||
|             signing_key=keypair, |             signing_key=keypair, | ||||||
|         ) |         ) | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|  | |||||||
| @ -1,22 +1,19 @@ | |||||||
| """authentik events signal listener""" | """authentik events signal listener""" | ||||||
|  |  | ||||||
| from importlib import import_module |  | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from django.conf import settings |  | ||||||
| from django.contrib.auth.signals import user_logged_in, user_logged_out | from django.contrib.auth.signals import user_logged_in, user_logged_out | ||||||
| from django.db.models.signals import post_save, pre_delete | from django.db.models.signals import post_save, pre_delete | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from rest_framework.request import Request |  | ||||||
|  |  | ||||||
| from authentik.core.models import AuthenticatedSession, User | from authentik.core.models import User | ||||||
| from authentik.core.signals import login_failed, password_changed | from authentik.core.signals import login_failed, password_changed | ||||||
| from authentik.events.apps import SYSTEM_TASK_STATUS | from authentik.events.apps import SYSTEM_TASK_STATUS | ||||||
| from authentik.events.models import Event, EventAction, SystemTask | from authentik.events.models import Event, EventAction, SystemTask | ||||||
| from authentik.events.tasks import event_notification_handler, gdpr_cleanup | from authentik.events.tasks import event_notification_handler, gdpr_cleanup | ||||||
| from authentik.flows.models import Stage | from authentik.flows.models import Stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_OUTPOST, PLAN_CONTEXT_SOURCE, FlowPlan | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.root.monitoring import monitoring_set | from authentik.root.monitoring import monitoring_set | ||||||
| from authentik.stages.invitation.models import Invitation | from authentik.stages.invitation.models import Invitation | ||||||
| @ -26,7 +23,6 @@ from authentik.stages.user_write.signals import user_write | |||||||
| from authentik.tenants.utils import get_current_tenant | from authentik.tenants.utils import get_current_tenant | ||||||
|  |  | ||||||
| SESSION_LOGIN_EVENT = "login_event" | SESSION_LOGIN_EVENT = "login_event" | ||||||
| _session_engine = import_module(settings.SESSION_ENGINE) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(user_logged_in) | @receiver(user_logged_in) | ||||||
| @ -42,22 +38,16 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_): | |||||||
|             # Save the login method used |             # Save the login method used | ||||||
|             kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD] |             kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD] | ||||||
|             kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) |             kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) | ||||||
|  |         if PLAN_CONTEXT_OUTPOST in flow_plan.context: | ||||||
|  |             # Save outpost context | ||||||
|  |             kwargs[PLAN_CONTEXT_OUTPOST] = flow_plan.context[PLAN_CONTEXT_OUTPOST] | ||||||
|     event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user) |     event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user) | ||||||
|     request.session[SESSION_LOGIN_EVENT] = event |     request.session[SESSION_LOGIN_EVENT] = event | ||||||
|     request.session.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_login_event(request_or_session: HttpRequest | AuthenticatedSession | None) -> Event | None: | def get_login_event(request: HttpRequest) -> Event | None: | ||||||
|     """Wrapper to get login event that can be mocked in tests""" |     """Wrapper to get login event that can be mocked in tests""" | ||||||
|     session = None |     return request.session.get(SESSION_LOGIN_EVENT, None) | ||||||
|     if not request_or_session: |  | ||||||
|         return None |  | ||||||
|     if isinstance(request_or_session, HttpRequest | Request): |  | ||||||
|         session = request_or_session.session |  | ||||||
|     if isinstance(request_or_session, AuthenticatedSession): |  | ||||||
|         SessionStore = _session_engine.SessionStore |  | ||||||
|         session = SessionStore(request_or_session.session_key) |  | ||||||
|     return session.get(SESSION_LOGIN_EVENT, None) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(user_logged_out) | @receiver(user_logged_out) | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ from authentik.flows.models import ( | |||||||
|     in_memory_stage, |     in_memory_stage, | ||||||
| ) | ) | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
|  | from authentik.outposts.models import Outpost | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| from authentik.root.middleware import ClientIPMiddleware | from authentik.root.middleware import ClientIPMiddleware | ||||||
|  |  | ||||||
| @ -32,6 +33,7 @@ PLAN_CONTEXT_SSO = "is_sso" | |||||||
| PLAN_CONTEXT_REDIRECT = "redirect" | PLAN_CONTEXT_REDIRECT = "redirect" | ||||||
| PLAN_CONTEXT_APPLICATION = "application" | PLAN_CONTEXT_APPLICATION = "application" | ||||||
| PLAN_CONTEXT_SOURCE = "source" | PLAN_CONTEXT_SOURCE = "source" | ||||||
|  | PLAN_CONTEXT_OUTPOST = "outpost" | ||||||
| # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan | # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan | ||||||
| # was restored. | # was restored. | ||||||
| PLAN_CONTEXT_IS_RESTORED = "is_restored" | PLAN_CONTEXT_IS_RESTORED = "is_restored" | ||||||
| @ -143,10 +145,23 @@ class FlowPlanner: | |||||||
|             and not request.user.is_superuser |             and not request.user.is_superuser | ||||||
|         ): |         ): | ||||||
|             raise FlowNonApplicableException() |             raise FlowNonApplicableException() | ||||||
|         if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST: |  | ||||||
|         outpost_user = ClientIPMiddleware.get_outpost_user(request) |         outpost_user = ClientIPMiddleware.get_outpost_user(request) | ||||||
|  |         if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST: | ||||||
|             if not outpost_user: |             if not outpost_user: | ||||||
|                 raise FlowNonApplicableException() |                 raise FlowNonApplicableException() | ||||||
|  |         if outpost_user: | ||||||
|  |             outpost = Outpost.objects.filter( | ||||||
|  |                 # TODO: Since Outpost and user are not directly connected, we have to look up a user | ||||||
|  |                 # like this. This should ideally by in authentik/outposts/models.py | ||||||
|  |                 pk=outpost_user.username.replace("ak-outpost-", "") | ||||||
|  |             ).first() | ||||||
|  |             if outpost: | ||||||
|  |                 return { | ||||||
|  |                     PLAN_CONTEXT_OUTPOST: { | ||||||
|  |                         "instance": outpost, | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |         return {} | ||||||
|  |  | ||||||
|     def plan(self, request: HttpRequest, default_context: dict[str, Any] | None = None) -> FlowPlan: |     def plan(self, request: HttpRequest, default_context: dict[str, Any] | None = None) -> FlowPlan: | ||||||
|         """Check each of the flows' policies, check policies for each stage with PolicyBinding |         """Check each of the flows' policies, check policies for each stage with PolicyBinding | ||||||
| @ -159,11 +174,12 @@ class FlowPlanner: | |||||||
|             self._logger.debug( |             self._logger.debug( | ||||||
|                 "f(plan): starting planning process", |                 "f(plan): starting planning process", | ||||||
|             ) |             ) | ||||||
|  |             context = default_context or {} | ||||||
|             # Bit of a workaround here, if there is a pending user set in the default context |             # Bit of a workaround here, if there is a pending user set in the default context | ||||||
|             # we use that user for our cache key |             # we use that user for our cache key | ||||||
|             # to make sure they don't get the generic response |             # to make sure they don't get the generic response | ||||||
|             if default_context and PLAN_CONTEXT_PENDING_USER in default_context: |             if context and PLAN_CONTEXT_PENDING_USER in context: | ||||||
|                 user = default_context[PLAN_CONTEXT_PENDING_USER] |                 user = context[PLAN_CONTEXT_PENDING_USER] | ||||||
|             else: |             else: | ||||||
|                 user = request.user |                 user = request.user | ||||||
|                 # We only need to check the flow authentication if it's planned without a user |                 # We only need to check the flow authentication if it's planned without a user | ||||||
| @ -171,14 +187,13 @@ class FlowPlanner: | |||||||
|                 # or if a flow is restarted due to `invalid_response_action` being set to |                 # or if a flow is restarted due to `invalid_response_action` being set to | ||||||
|                 # `restart_with_context`, which can only happen if the user was already authorized |                 # `restart_with_context`, which can only happen if the user was already authorized | ||||||
|                 # to use the flow |                 # to use the flow | ||||||
|                 self._check_authentication(request) |                 context.update(self._check_authentication(request)) | ||||||
|             # First off, check the flow's direct policy bindings |             # First off, check the flow's direct policy bindings | ||||||
|             # to make sure the user even has access to the flow |             # to make sure the user even has access to the flow | ||||||
|             engine = PolicyEngine(self.flow, user, request) |             engine = PolicyEngine(self.flow, user, request) | ||||||
|             engine.use_cache = self.use_cache |             engine.use_cache = self.use_cache | ||||||
|             if default_context: |             span.set_data("context", cleanse_dict(context)) | ||||||
|                 span.set_data("default_context", cleanse_dict(default_context)) |             engine.request.context.update(context) | ||||||
|                 engine.request.context.update(default_context) |  | ||||||
|             engine.build() |             engine.build() | ||||||
|             result = engine.result |             result = engine.result | ||||||
|             if not result.passing: |             if not result.passing: | ||||||
| @ -195,12 +210,12 @@ class FlowPlanner: | |||||||
|                         key=cached_plan_key, |                         key=cached_plan_key, | ||||||
|                     ) |                     ) | ||||||
|                     # Reset the context as this isn't factored into caching |                     # Reset the context as this isn't factored into caching | ||||||
|                     cached_plan.context = default_context or {} |                     cached_plan.context = context | ||||||
|                     return cached_plan |                     return cached_plan | ||||||
|             self._logger.debug( |             self._logger.debug( | ||||||
|                 "f(plan): building plan", |                 "f(plan): building plan", | ||||||
|             ) |             ) | ||||||
|             plan = self._build_plan(user, request, default_context) |             plan = self._build_plan(user, request, context) | ||||||
|             if self.use_cache: |             if self.use_cache: | ||||||
|                 cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) |                 cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) | ||||||
|             if not plan.bindings and not self.allow_empty_flows: |             if not plan.bindings and not self.allow_empty_flows: | ||||||
|  | |||||||
| @ -21,14 +21,7 @@ class DebugSession(Session): | |||||||
|  |  | ||||||
|     def send(self, req: PreparedRequest, *args, **kwargs): |     def send(self, req: PreparedRequest, *args, **kwargs): | ||||||
|         request_id = str(uuid4()) |         request_id = str(uuid4()) | ||||||
|         LOGGER.debug( |         LOGGER.debug("HTTP request sent", uid=request_id, path=req.path_url, headers=req.headers) | ||||||
|             "HTTP request sent", |  | ||||||
|             uid=request_id, |  | ||||||
|             url=req.url, |  | ||||||
|             method=req.method, |  | ||||||
|             headers=req.headers, |  | ||||||
|             body=req.body, |  | ||||||
|         ) |  | ||||||
|         resp = super().send(req, *args, **kwargs) |         resp = super().send(req, *args, **kwargs) | ||||||
|         LOGGER.debug( |         LOGGER.debug( | ||||||
|             "HTTP response received", |             "HTTP response received", | ||||||
|  | |||||||
| @ -108,7 +108,7 @@ class EventMatcherPolicy(Policy): | |||||||
|                 result=result, |                 result=result, | ||||||
|             ) |             ) | ||||||
|             matches.append(result) |             matches.append(result) | ||||||
|         passing = all(x.passing for x in matches) |         passing = any(x.passing for x in matches) | ||||||
|         messages = chain(*[x.messages for x in matches]) |         messages = chain(*[x.messages for x in matches]) | ||||||
|         result = PolicyResult(passing, *messages) |         result = PolicyResult(passing, *messages) | ||||||
|         result.source_results = matches |         result.source_results = matches | ||||||
|  | |||||||
| @ -77,24 +77,11 @@ class TestEventMatcherPolicy(TestCase): | |||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["event"] = event |         request.context["event"] = event | ||||||
|         policy: EventMatcherPolicy = EventMatcherPolicy.objects.create( |         policy: EventMatcherPolicy = EventMatcherPolicy.objects.create( | ||||||
|             client_ip="1.2.3.5", app="foo" |             client_ip="1.2.3.5", app="bar" | ||||||
|         ) |         ) | ||||||
|         response = policy.passes(request) |         response = policy.passes(request) | ||||||
|         self.assertFalse(response.passing) |         self.assertFalse(response.passing) | ||||||
|  |  | ||||||
|     def test_multiple(self): |  | ||||||
|         """Test multiple""" |  | ||||||
|         event = Event.new(EventAction.LOGIN) |  | ||||||
|         event.app = "foo" |  | ||||||
|         event.client_ip = "1.2.3.4" |  | ||||||
|         request = PolicyRequest(get_anonymous_user()) |  | ||||||
|         request.context["event"] = event |  | ||||||
|         policy: EventMatcherPolicy = EventMatcherPolicy.objects.create( |  | ||||||
|             client_ip="1.2.3.4", app="foo" |  | ||||||
|         ) |  | ||||||
|         response = policy.passes(request) |  | ||||||
|         self.assertTrue(response.passing) |  | ||||||
|  |  | ||||||
|     def test_invalid(self): |     def test_invalid(self): | ||||||
|         """Test passing event""" |         """Test passing event""" | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  | |||||||
| @ -1,18 +1,15 @@ | |||||||
| """OAuth2Provider API Views""" | """OAuth2Provider API Views""" | ||||||
|  |  | ||||||
| from copy import copy | from copy import copy | ||||||
| from re import compile |  | ||||||
| from re import error as RegexError |  | ||||||
|  |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.translation import gettext_lazy as _ |  | ||||||
| from drf_spectacular.types import OpenApiTypes | from drf_spectacular.types import OpenApiTypes | ||||||
| from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | ||||||
| from guardian.shortcuts import get_objects_for_user | from guardian.shortcuts import get_objects_for_user | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.fields import CharField, ChoiceField | from rest_framework.fields import CharField | ||||||
| from rest_framework.generics import get_object_or_404 | from rest_framework.generics import get_object_or_404 | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| @ -23,39 +20,13 @@ 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 ( | from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping | ||||||
|     AccessToken, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.rbac.decorators import permission_required | from authentik.rbac.decorators import permission_required | ||||||
|  |  | ||||||
|  |  | ||||||
| class RedirectURISerializer(PassiveSerializer): |  | ||||||
|     """A single allowed redirect URI entry""" |  | ||||||
|  |  | ||||||
|     matching_mode = ChoiceField(choices=RedirectURIMatchingMode.choices) |  | ||||||
|     url = CharField() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class OAuth2ProviderSerializer(ProviderSerializer): | class OAuth2ProviderSerializer(ProviderSerializer): | ||||||
|     """OAuth2Provider Serializer""" |     """OAuth2Provider Serializer""" | ||||||
|  |  | ||||||
|     redirect_uris = RedirectURISerializer(many=True, source="_redirect_uris") |  | ||||||
|  |  | ||||||
|     def validate_redirect_uris(self, data: list) -> list: |  | ||||||
|         for entry in data: |  | ||||||
|             if entry.get("matching_mode") == RedirectURIMatchingMode.REGEX: |  | ||||||
|                 url = entry.get("url") |  | ||||||
|                 try: |  | ||||||
|                     compile(url) |  | ||||||
|                 except RegexError: |  | ||||||
|                     raise ValidationError( |  | ||||||
|                         _("Invalid Regex Pattern: {url}".format(url=url)) |  | ||||||
|                     ) from None |  | ||||||
|         return data |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = OAuth2Provider |         model = OAuth2Provider | ||||||
|         fields = ProviderSerializer.Meta.fields + [ |         fields = ProviderSerializer.Meta.fields + [ | ||||||
| @ -107,6 +78,7 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet): | |||||||
|         "refresh_token_validity", |         "refresh_token_validity", | ||||||
|         "include_claims_in_id_token", |         "include_claims_in_id_token", | ||||||
|         "signing_key", |         "signing_key", | ||||||
|  |         "redirect_uris", | ||||||
|         "sub_mode", |         "sub_mode", | ||||||
|         "property_mappings", |         "property_mappings", | ||||||
|         "issuer_mode", |         "issuer_mode", | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect | |||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.lib.views import bad_request_message | from authentik.lib.views import bad_request_message | ||||||
| from authentik.providers.oauth2.models import GrantTypes, RedirectURI | from authentik.providers.oauth2.models import GrantTypes | ||||||
|  |  | ||||||
|  |  | ||||||
| class OAuth2Error(SentryIgnoredException): | class OAuth2Error(SentryIgnoredException): | ||||||
| @ -46,9 +46,9 @@ class RedirectUriError(OAuth2Error): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     provided_uri: str |     provided_uri: str | ||||||
|     allowed_uris: list[RedirectURI] |     allowed_uris: list[str] | ||||||
|  |  | ||||||
|     def __init__(self, provided_uri: str, allowed_uris: list[RedirectURI]) -> None: |     def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None: | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.provided_uri = provided_uri |         self.provided_uri = provided_uri | ||||||
|         self.allowed_uris = allowed_uris |         self.allowed_uris = allowed_uris | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """id_token utils""" | """id_token utils""" | ||||||
|  |  | ||||||
| from dataclasses import asdict, dataclass, field | from dataclasses import asdict, dataclass, field | ||||||
| from hashlib import sha256 |  | ||||||
| from typing import TYPE_CHECKING, Any | from typing import TYPE_CHECKING, Any | ||||||
|  |  | ||||||
| from django.db import models | from django.db import models | ||||||
| @ -24,13 +23,8 @@ if TYPE_CHECKING: | |||||||
|     from authentik.providers.oauth2.models import BaseGrantModel, OAuth2Provider |     from authentik.providers.oauth2.models import BaseGrantModel, OAuth2Provider | ||||||
|  |  | ||||||
|  |  | ||||||
| def hash_session_key(session_key: str) -> str: |  | ||||||
|     """Hash the session key for inclusion in JWTs as `sid`""" |  | ||||||
|     return sha256(session_key.encode("ascii")).hexdigest() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SubModes(models.TextChoices): | class SubModes(models.TextChoices): | ||||||
|     """Mode after which 'sub' attribute is generated, for compatibility reasons""" |     """Mode after which 'sub' attribute is generateed, for compatibility reasons""" | ||||||
|  |  | ||||||
|     HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID") |     HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID") | ||||||
|     USER_ID = "user_id", _("Based on user ID") |     USER_ID = "user_id", _("Based on user ID") | ||||||
| @ -57,8 +51,7 @@ class IDToken: | |||||||
|     and potentially other requested Claims. The ID Token is represented as a |     and potentially other requested Claims. The ID Token is represented as a | ||||||
|     JSON Web Token (JWT) [JWT]. |     JSON Web Token (JWT) [JWT]. | ||||||
|  |  | ||||||
|     https://openid.net/specs/openid-connect-core-1_0.html#IDToken |     https://openid.net/specs/openid-connect-core-1_0.html#IDToken""" | ||||||
|     https://www.iana.org/assignments/jwt/jwt.xhtml""" |  | ||||||
|  |  | ||||||
|     # Issuer, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1 |     # Issuer, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1 | ||||||
|     iss: str | None = None |     iss: str | None = None | ||||||
| @ -86,8 +79,6 @@ class IDToken: | |||||||
|     nonce: str | None = None |     nonce: str | None = None | ||||||
|     # Access Token hash value, http://openid.net/specs/openid-connect-core-1_0.html |     # Access Token hash value, http://openid.net/specs/openid-connect-core-1_0.html | ||||||
|     at_hash: str | None = None |     at_hash: str | None = None | ||||||
|     # Session ID, https://openid.net/specs/openid-connect-frontchannel-1_0.html#ClaimsContents |  | ||||||
|     sid: str | None = None |  | ||||||
|  |  | ||||||
|     claims: dict[str, Any] = field(default_factory=dict) |     claims: dict[str, Any] = field(default_factory=dict) | ||||||
|  |  | ||||||
| @ -125,11 +116,9 @@ class IDToken: | |||||||
|         now = timezone.now() |         now = timezone.now() | ||||||
|         id_token.iat = int(now.timestamp()) |         id_token.iat = int(now.timestamp()) | ||||||
|         id_token.auth_time = int(token.auth_time.timestamp()) |         id_token.auth_time = int(token.auth_time.timestamp()) | ||||||
|         if token.session: |  | ||||||
|             id_token.sid = hash_session_key(token.session.session_key) |  | ||||||
|  |  | ||||||
|         # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time |         # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time | ||||||
|         auth_event = get_login_event(token.session) |         auth_event = get_login_event(request) | ||||||
|         if auth_event: |         if auth_event: | ||||||
|             # Also check which method was used for authentication |             # Also check which method was used for authentication | ||||||
|             method = auth_event.context.get(PLAN_CONTEXT_METHOD, "") |             method = auth_event.context.get(PLAN_CONTEXT_METHOD, "") | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ | |||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
| from django.apps.registry import Apps | from django.apps.registry import Apps | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor |  | ||||||
|  |  | ||||||
| import authentik.lib.utils.time | import authentik.lib.utils.time | ||||||
|  |  | ||||||
| @ -15,7 +14,7 @@ scope_uid_map = { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| def set_managed_flag(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | def set_managed_flag(apps: Apps, schema_editor): | ||||||
|     ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping") |     ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping") | ||||||
|     db_alias = schema_editor.connection.alias |     db_alias = schema_editor.connection.alias | ||||||
|     for mapping in ScopeMapping.objects.using(db_alias).filter(name__startswith="Autogenerated "): |     for mapping in ScopeMapping.objects.using(db_alias).filter(name__startswith="Autogenerated "): | ||||||
|  | |||||||
| @ -11,16 +11,13 @@ class Migration(migrations.Migration): | |||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     # Original preserved |     operations = [ | ||||||
|     # See https://github.com/goauthentik/authentik/issues/11874 |         migrations.AddIndex( | ||||||
|     # operations = [ |             model_name="accesstoken", | ||||||
|     #     migrations.AddIndex( |             index=models.Index(fields=["token"], name="authentik_p_token_4bc870_idx"), | ||||||
|     #         model_name="accesstoken", |         ), | ||||||
|     #         index=models.Index(fields=["token"], name="authentik_p_token_4bc870_idx"), |         migrations.AddIndex( | ||||||
|     #     ), |             model_name="refreshtoken", | ||||||
|     #     migrations.AddIndex( |             index=models.Index(fields=["token"], name="authentik_p_token_1a841f_idx"), | ||||||
|     #         model_name="refreshtoken", |         ), | ||||||
|     #         index=models.Index(fields=["token"], name="authentik_p_token_1a841f_idx"), |     ] | ||||||
|     #     ), |  | ||||||
|     # ] |  | ||||||
|     operations = [] |  | ||||||
|  | |||||||
| @ -11,24 +11,21 @@ class Migration(migrations.Migration): | |||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     # Original preserved |     operations = [ | ||||||
|     # See https://github.com/goauthentik/authentik/issues/11874 |         migrations.RemoveIndex( | ||||||
|     # operations = [ |             model_name="accesstoken", | ||||||
|     #     migrations.RemoveIndex( |             name="authentik_p_token_4bc870_idx", | ||||||
|     #         model_name="accesstoken", |         ), | ||||||
|     #         name="authentik_p_token_4bc870_idx", |         migrations.RemoveIndex( | ||||||
|     #     ), |             model_name="refreshtoken", | ||||||
|     #     migrations.RemoveIndex( |             name="authentik_p_token_1a841f_idx", | ||||||
|     #         model_name="refreshtoken", |         ), | ||||||
|     #         name="authentik_p_token_1a841f_idx", |         migrations.AddIndex( | ||||||
|     #     ), |             model_name="accesstoken", | ||||||
|     #     migrations.AddIndex( |             index=models.Index(fields=["token", "provider"], name="authentik_p_token_f99422_idx"), | ||||||
|     #         model_name="accesstoken", |         ), | ||||||
|     #         index=models.Index(fields=["token", "provider"], name="authentik_p_token_f99422_idx"), |         migrations.AddIndex( | ||||||
|     #     ), |             model_name="refreshtoken", | ||||||
|     #     migrations.AddIndex( |             index=models.Index(fields=["token", "provider"], name="authentik_p_token_a1d921_idx"), | ||||||
|     #         model_name="refreshtoken", |         ), | ||||||
|     #         index=models.Index(fields=["token", "provider"], name="authentik_p_token_a1d921_idx"), |     ] | ||||||
|     #     ), |  | ||||||
|     # ] |  | ||||||
|     operations = [] |  | ||||||
|  | |||||||
| @ -1,42 +0,0 @@ | |||||||
| # Generated by Django 5.0.9 on 2024-10-16 14:53 |  | ||||||
|  |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("authentik_crypto", "0004_alter_certificatekeypair_name"), |  | ||||||
|         ( |  | ||||||
|             "authentik_providers_oauth2", |  | ||||||
|             "0020_remove_accesstoken_authentik_p_token_4bc870_idx_and_more", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="oauth2provider", |  | ||||||
|             name="encryption_key", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 help_text="Key used to encrypt the tokens. When set, tokens will be encrypted and returned as JWEs.", |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_NULL, |  | ||||||
|                 related_name="oauth2provider_encryption_key_set", |  | ||||||
|                 to="authentik_crypto.certificatekeypair", |  | ||||||
|                 verbose_name="Encryption Key", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name="oauth2provider", |  | ||||||
|             name="signing_key", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 help_text="Key used to sign the tokens.", |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_NULL, |  | ||||||
|                 related_name="oauth2provider_signing_key_set", |  | ||||||
|                 to="authentik_crypto.certificatekeypair", |  | ||||||
|                 verbose_name="Signing Key", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,113 +0,0 @@ | |||||||
| # Generated by Django 5.0.9 on 2024-10-23 13:38 |  | ||||||
|  |  | ||||||
| from hashlib import sha256 |  | ||||||
| import django.db.models.deletion |  | ||||||
| from django.db import migrations, models |  | ||||||
| from django.apps.registry import Apps |  | ||||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor |  | ||||||
| from authentik.lib.migrations import progress_bar |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def migrate_session(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): |  | ||||||
|     AuthenticatedSession = apps.get_model("authentik_core", "authenticatedsession") |  | ||||||
|     AuthorizationCode = apps.get_model("authentik_providers_oauth2", "authorizationcode") |  | ||||||
|     AccessToken = apps.get_model("authentik_providers_oauth2", "accesstoken") |  | ||||||
|     RefreshToken = apps.get_model("authentik_providers_oauth2", "refreshtoken") |  | ||||||
|     db_alias = schema_editor.connection.alias |  | ||||||
|  |  | ||||||
|     print(f"\nFetching session keys, this might take a couple of minutes...") |  | ||||||
|     session_ids = {} |  | ||||||
|     for session in progress_bar(AuthenticatedSession.objects.using(db_alias).all()): |  | ||||||
|         session_ids[sha256(session.session_key.encode("ascii")).hexdigest()] = session.session_key |  | ||||||
|     for model in [AuthorizationCode, AccessToken, RefreshToken]: |  | ||||||
|         print( |  | ||||||
|             f"\nAdding session to {model._meta.verbose_name}, this might take a couple of minutes..." |  | ||||||
|         ) |  | ||||||
|         for code in progress_bar(model.objects.using(db_alias).all()): |  | ||||||
|             if code.session_id_old not in session_ids: |  | ||||||
|                 continue |  | ||||||
|             code.session = ( |  | ||||||
|                 AuthenticatedSession.objects.using(db_alias) |  | ||||||
|                 .filter(session_key=session_ids[code.session_id_old]) |  | ||||||
|                 .first() |  | ||||||
|             ) |  | ||||||
|             code.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"), |  | ||||||
|         ("authentik_providers_oauth2", "0021_oauth2provider_encryption_key_and_more"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="accesstoken", |  | ||||||
|             old_name="session_id", |  | ||||||
|             new_name="session_id_old", |  | ||||||
|         ), |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="authorizationcode", |  | ||||||
|             old_name="session_id", |  | ||||||
|             new_name="session_id_old", |  | ||||||
|         ), |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="refreshtoken", |  | ||||||
|             old_name="session_id", |  | ||||||
|             new_name="session_id_old", |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="accesstoken", |  | ||||||
|             name="session", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, |  | ||||||
|                 to="authentik_core.authenticatedsession", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="authorizationcode", |  | ||||||
|             name="session", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, |  | ||||||
|                 to="authentik_core.authenticatedsession", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="devicetoken", |  | ||||||
|             name="session", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, |  | ||||||
|                 to="authentik_core.authenticatedsession", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="refreshtoken", |  | ||||||
|             name="session", |  | ||||||
|             field=models.ForeignKey( |  | ||||||
|                 default=None, |  | ||||||
|                 null=True, |  | ||||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, |  | ||||||
|                 to="authentik_core.authenticatedsession", |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython(migrate_session), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name="accesstoken", |  | ||||||
|             name="session_id_old", |  | ||||||
|         ), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name="authorizationcode", |  | ||||||
|             name="session_id_old", |  | ||||||
|         ), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name="refreshtoken", |  | ||||||
|             name="session_id_old", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| # Generated by Django 5.0.9 on 2024-10-31 14:28 |  | ||||||
|  |  | ||||||
| import django.contrib.postgres.indexes |  | ||||||
| from django.conf import settings |  | ||||||
| from django.db import migrations |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"), |  | ||||||
|         ("authentik_providers_oauth2", "0022_remove_accesstoken_session_id_and_more"), |  | ||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RunSQL("DROP INDEX IF EXISTS authentik_p_token_f99422_idx;"), |  | ||||||
|         migrations.RunSQL("DROP INDEX IF EXISTS authentik_p_token_a1d921_idx;"), |  | ||||||
|         migrations.AddIndex( |  | ||||||
|             model_name="accesstoken", |  | ||||||
|             index=django.contrib.postgres.indexes.HashIndex( |  | ||||||
|                 fields=["token"], name="authentik_p_token_e00883_hash" |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|         migrations.AddIndex( |  | ||||||
|             model_name="refreshtoken", |  | ||||||
|             index=django.contrib.postgres.indexes.HashIndex( |  | ||||||
|                 fields=["token"], name="authentik_p_token_32e2b7_hash" |  | ||||||
|             ), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,49 +0,0 @@ | |||||||
| # Generated by Django 5.0.9 on 2024-11-04 12:56 |  | ||||||
| from dataclasses import asdict |  | ||||||
| from django.apps.registry import Apps |  | ||||||
|  |  | ||||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def migrate_redirect_uris(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): |  | ||||||
|     from authentik.providers.oauth2.models import RedirectURI, RedirectURIMatchingMode |  | ||||||
|  |  | ||||||
|     OAuth2Provider = apps.get_model("authentik_providers_oauth2", "oauth2provider") |  | ||||||
|  |  | ||||||
|     db_alias = schema_editor.connection.alias |  | ||||||
|     for provider in OAuth2Provider.objects.using(db_alias).all(): |  | ||||||
|         uris = [] |  | ||||||
|         for old in provider.old_redirect_uris.split("\n"): |  | ||||||
|             mode = RedirectURIMatchingMode.STRICT |  | ||||||
|             if old == "*" or old == ".*": |  | ||||||
|                 mode = RedirectURIMatchingMode.REGEX |  | ||||||
|             uris.append(asdict(RedirectURI(mode, url=old))) |  | ||||||
|         provider._redirect_uris = uris |  | ||||||
|         provider.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("authentik_providers_oauth2", "0023_alter_accesstoken_refreshtoken_use_hash_index"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name="oauth2provider", |  | ||||||
|             old_name="redirect_uris", |  | ||||||
|             new_name="old_redirect_uris", |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name="oauth2provider", |  | ||||||
|             name="_redirect_uris", |  | ||||||
|             field=models.JSONField(default=dict, verbose_name="Redirect URIs"), |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython(migrate_redirect_uris, lambda *args: ...), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name="oauth2provider", |  | ||||||
|             name="old_redirect_uris", |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -3,7 +3,7 @@ | |||||||
| import base64 | import base64 | ||||||
| import binascii | import binascii | ||||||
| import json | import json | ||||||
| from dataclasses import asdict, dataclass | from dataclasses import asdict | ||||||
| from functools import cached_property | from functools import cached_property | ||||||
| from hashlib import sha256 | from hashlib import sha256 | ||||||
| from typing import Any | from typing import Any | ||||||
| @ -12,7 +12,6 @@ from urllib.parse import urlparse, urlunparse | |||||||
| from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey | from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey | ||||||
| from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey | ||||||
| from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes | from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes | ||||||
| from dacite import Config |  | ||||||
| from dacite.core import from_dict | from dacite.core import from_dict | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| @ -24,13 +23,7 @@ from rest_framework.serializers import Serializer | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.brands.models import WebfingerProvider | from authentik.brands.models import WebfingerProvider | ||||||
| from authentik.core.models import ( | from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User | ||||||
|     AuthenticatedSession, |  | ||||||
|     ExpiringModel, |  | ||||||
|     PropertyMapping, |  | ||||||
|     Provider, |  | ||||||
|     User, |  | ||||||
| ) |  | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key | from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key | ||||||
| from authentik.lib.models import SerializerModel | from authentik.lib.models import SerializerModel | ||||||
| @ -74,25 +67,11 @@ class IssuerMode(models.TextChoices): | |||||||
|     """Configure how the `iss` field is created.""" |     """Configure how the `iss` field is created.""" | ||||||
|  |  | ||||||
|     GLOBAL = "global", _("Same identifier is used for all providers") |     GLOBAL = "global", _("Same identifier is used for all providers") | ||||||
|     PER_PROVIDER = ( |     PER_PROVIDER = "per_provider", _( | ||||||
|         "per_provider", |         "Each provider has a different issuer, based on the application slug." | ||||||
|         _("Each provider has a different issuer, based on the application slug."), |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RedirectURIMatchingMode(models.TextChoices): |  | ||||||
|     STRICT = "strict", _("Strict URL comparison") |  | ||||||
|     REGEX = "regex", _("Regular Expression URL matching") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass |  | ||||||
| class RedirectURI: |  | ||||||
|     """A single redirect URI entry""" |  | ||||||
|  |  | ||||||
|     matching_mode: RedirectURIMatchingMode |  | ||||||
|     url: str |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ResponseTypes(models.TextChoices): | class ResponseTypes(models.TextChoices): | ||||||
|     """Response Type required by the client.""" |     """Response Type required by the client.""" | ||||||
|  |  | ||||||
| @ -167,9 +146,11 @@ class OAuth2Provider(WebfingerProvider, Provider): | |||||||
|         verbose_name=_("Client Secret"), |         verbose_name=_("Client Secret"), | ||||||
|         default=generate_client_secret, |         default=generate_client_secret, | ||||||
|     ) |     ) | ||||||
|     _redirect_uris = models.JSONField( |     redirect_uris = models.TextField( | ||||||
|         default=dict, |         default="", | ||||||
|  |         blank=True, | ||||||
|         verbose_name=_("Redirect URIs"), |         verbose_name=_("Redirect URIs"), | ||||||
|  |         help_text=_("Enter each URI on a new line."), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     include_claims_in_id_token = models.BooleanField( |     include_claims_in_id_token = models.BooleanField( | ||||||
| @ -270,33 +251,12 @@ class OAuth2Provider(WebfingerProvider, Provider): | |||||||
|         except Provider.application.RelatedObjectDoesNotExist: |         except Provider.application.RelatedObjectDoesNotExist: | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def redirect_uris(self) -> list[RedirectURI]: |  | ||||||
|         uris = [] |  | ||||||
|         for entry in self._redirect_uris: |  | ||||||
|             uris.append( |  | ||||||
|                 from_dict( |  | ||||||
|                     RedirectURI, |  | ||||||
|                     entry, |  | ||||||
|                     config=Config(type_hooks={RedirectURIMatchingMode: RedirectURIMatchingMode}), |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         return uris |  | ||||||
|  |  | ||||||
|     @redirect_uris.setter |  | ||||||
|     def redirect_uris(self, value: list[RedirectURI]): |  | ||||||
|         cleansed = [] |  | ||||||
|         for entry in value: |  | ||||||
|             cleansed.append(asdict(entry)) |  | ||||||
|         self._redirect_uris = cleansed |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def launch_url(self) -> str | None: |     def launch_url(self) -> str | None: | ||||||
|         """Guess launch_url based on first redirect_uri""" |         """Guess launch_url based on first redirect_uri""" | ||||||
|         redirects = self.redirect_uris |         if self.redirect_uris == "": | ||||||
|         if len(redirects) < 1: |  | ||||||
|             return None |             return None | ||||||
|         main_url = redirects[0].url |         main_url = self.redirect_uris.split("\n", maxsplit=1)[0] | ||||||
|         try: |         try: | ||||||
|             launch_url = urlparse(main_url)._replace(path="") |             launch_url = urlparse(main_url)._replace(path="") | ||||||
|             return urlunparse(launch_url) |             return urlunparse(launch_url) | ||||||
| @ -360,9 +320,7 @@ class BaseGrantModel(models.Model): | |||||||
|     revoked = models.BooleanField(default=False) |     revoked = models.BooleanField(default=False) | ||||||
|     _scope = models.TextField(default="", verbose_name=_("Scopes")) |     _scope = models.TextField(default="", verbose_name=_("Scopes")) | ||||||
|     auth_time = models.DateTimeField(verbose_name="Authentication time") |     auth_time = models.DateTimeField(verbose_name="Authentication time") | ||||||
|     session = models.ForeignKey( |     session_id = models.CharField(default="", blank=True) | ||||||
|         AuthenticatedSession, null=True, on_delete=models.SET_DEFAULT, default=None |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         abstract = True |         abstract = True | ||||||
| @ -418,6 +376,9 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel): | |||||||
|     _id_token = models.TextField() |     _id_token = models.TextField() | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |         indexes = [ | ||||||
|  |             models.Index(fields=["token", "provider"]), | ||||||
|  |         ] | ||||||
|         verbose_name = _("OAuth2 Access Token") |         verbose_name = _("OAuth2 Access Token") | ||||||
|         verbose_name_plural = _("OAuth2 Access Tokens") |         verbose_name_plural = _("OAuth2 Access Tokens") | ||||||
|  |  | ||||||
| @ -461,6 +422,9 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel): | |||||||
|     _id_token = models.TextField(verbose_name=_("ID Token")) |     _id_token = models.TextField(verbose_name=_("ID Token")) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |         indexes = [ | ||||||
|  |             models.Index(fields=["token", "provider"]), | ||||||
|  |         ] | ||||||
|         verbose_name = _("OAuth2 Refresh Token") |         verbose_name = _("OAuth2 Refresh Token") | ||||||
|         verbose_name_plural = _("OAuth2 Refresh Tokens") |         verbose_name_plural = _("OAuth2 Refresh Tokens") | ||||||
|  |  | ||||||
| @ -494,9 +458,6 @@ class DeviceToken(ExpiringModel): | |||||||
|     device_code = models.TextField(default=generate_key) |     device_code = models.TextField(default=generate_key) | ||||||
|     user_code = models.TextField(default=generate_code_fixed_length) |     user_code = models.TextField(default=generate_code_fixed_length) | ||||||
|     _scope = models.TextField(default="", verbose_name=_("Scopes")) |     _scope = models.TextField(default="", verbose_name=_("Scopes")) | ||||||
|     session = models.ForeignKey( |  | ||||||
|         AuthenticatedSession, null=True, on_delete=models.SET_DEFAULT, default=None |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def scope(self) -> list[str]: |     def scope(self) -> list[str]: | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | from hashlib import sha256 | ||||||
|  |  | ||||||
| from django.contrib.auth.signals import user_logged_out | from django.contrib.auth.signals import user_logged_out | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| @ -11,4 +13,5 @@ def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, | |||||||
|     """Revoke access tokens upon user logout""" |     """Revoke access tokens upon user logout""" | ||||||
|     if not request.session or not request.session.session_key: |     if not request.session or not request.session.session_key: | ||||||
|         return |         return | ||||||
|     AccessToken.objects.filter(user=user, session__session_key=request.session.session_key).delete() |     hashed_session_key = sha256(request.session.session_key.encode("ascii")).hexdigest() | ||||||
|  |     AccessToken.objects.filter(user=user, session_id=hashed_session_key).delete() | ||||||
|  | |||||||
| @ -10,13 +10,7 @@ from rest_framework.test import APITestCase | |||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||||
| from authentik.providers.oauth2.models import ( |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestAPI(APITestCase): | class TestAPI(APITestCase): | ||||||
| @ -27,7 +21,7 @@ class TestAPI(APITestCase): | |||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|         ) |         ) | ||||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) |         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||||
|         self.app = Application.objects.create(name="test", slug="test", provider=self.provider) |         self.app = Application.objects.create(name="test", slug="test", provider=self.provider) | ||||||
| @ -56,29 +50,9 @@ class TestAPI(APITestCase): | |||||||
|     @skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up") |     @skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up") | ||||||
|     def test_launch_url(self): |     def test_launch_url(self): | ||||||
|         """Test launch_url""" |         """Test launch_url""" | ||||||
|         self.provider.redirect_uris = [ |         self.provider.redirect_uris = ( | ||||||
|             RedirectURI( |             "https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/\n" | ||||||
|                 RedirectURIMatchingMode.REGEX, |         ) | ||||||
|                 "https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/", |  | ||||||
|             ), |  | ||||||
|         ] |  | ||||||
|         self.provider.save() |         self.provider.save() | ||||||
|         self.provider.refresh_from_db() |         self.provider.refresh_from_db() | ||||||
|         self.assertIsNone(self.provider.launch_url) |         self.assertIsNone(self.provider.launch_url) | ||||||
|  |  | ||||||
|     def test_validate_redirect_uris(self): |  | ||||||
|         """Test redirect_uris API""" |  | ||||||
|         response = self.client.post( |  | ||||||
|             reverse("authentik_api:oauth2provider-list"), |  | ||||||
|             data={ |  | ||||||
|                 "name": generate_id(), |  | ||||||
|                 "authorization_flow": create_test_flow().pk, |  | ||||||
|                 "invalidation_flow": create_test_flow().pk, |  | ||||||
|                 "redirect_uris": [ |  | ||||||
|                     {"matching_mode": "strict", "url": "http://goauthentik.io"}, |  | ||||||
|                     {"matching_mode": "regex", "url": "**"}, |  | ||||||
|                 ], |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]}) |  | ||||||
|         self.assertEqual(response.status_code, 400) |  | ||||||
|  | |||||||
| @ -19,8 +19,6 @@ from authentik.providers.oauth2.models import ( | |||||||
|     AuthorizationCode, |     AuthorizationCode, | ||||||
|     GrantTypes, |     GrantTypes, | ||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
| @ -41,7 +39,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], |             redirect_uris="http://local.invalid/Foo", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(AuthorizeError): |         with self.assertRaises(AuthorizeError): | ||||||
|             request = self.factory.get( |             request = self.factory.get( | ||||||
| @ -66,7 +64,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], |             redirect_uris="http://local.invalid/Foo", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(AuthorizeError): |         with self.assertRaises(AuthorizeError): | ||||||
|             request = self.factory.get( |             request = self.factory.get( | ||||||
| @ -86,7 +84,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) |             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||||
| @ -108,7 +106,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")], |             redirect_uris="data:local.invalid", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
|             request = self.factory.get( |             request = self.factory.get( | ||||||
| @ -127,7 +125,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[], |             redirect_uris="", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) |             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||||
| @ -142,7 +140,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         ) |         ) | ||||||
|         OAuthAuthorizationParams.from_request(request) |         OAuthAuthorizationParams.from_request(request) | ||||||
|         provider.refresh_from_db() |         provider.refresh_from_db() | ||||||
|         self.assertEqual(provider.redirect_uris, [RedirectURI(RedirectURIMatchingMode.STRICT, "+")]) |         self.assertEqual(provider.redirect_uris, "+") | ||||||
|  |  | ||||||
|     def test_invalid_redirect_uri_regex(self): |     def test_invalid_redirect_uri_regex(self): | ||||||
|         """test missing/invalid redirect URI""" |         """test missing/invalid redirect URI""" | ||||||
| @ -150,7 +148,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")], |             redirect_uris="http://local.invalid?", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) |             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||||
| @ -172,7 +170,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")], |             redirect_uris="+", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) |             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||||
| @ -215,7 +213,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], |             redirect_uris="http://local.invalid/Foo", | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
| @ -303,7 +301,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], |             redirect_uris="foo://localhost", | ||||||
|             access_code_validity="seconds=100", |             access_code_validity="seconds=100", | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
| @ -345,7 +343,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -421,7 +419,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
| @ -476,7 +474,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |             client_id=generate_id(), | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -534,7 +532,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |             client_id=generate_id(), | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) |         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||||
|  | |||||||
| @ -11,14 +11,7 @@ from authentik.core.models import Application | |||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT | from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken | ||||||
|     AccessToken, |  | ||||||
|     IDToken, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     RefreshToken, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -30,7 +23,7 @@ class TesOAuth2Introspection(OAuthTestCase): | |||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.app = Application.objects.create( |         self.app = Application.objects.create( | ||||||
| @ -125,7 +118,7 @@ class TesOAuth2Introspection(OAuthTestCase): | |||||||
|         provider: OAuth2Provider = OAuth2Provider.objects.create( |         provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() |         auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ from authentik.core.tests.utils import create_test_cert, create_test_flow | |||||||
| from authentik.crypto.builder import PrivateKeyAlg | from authentik.crypto.builder import PrivateKeyAlg | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode | from authentik.providers.oauth2.models import OAuth2Provider | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
| TEST_CORDS_CERT = """ | TEST_CORDS_CERT = """ | ||||||
| @ -49,7 +49,7 @@ class TestJWKS(OAuthTestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create(name="test", slug="test", provider=provider) |         app = Application.objects.create(name="test", slug="test", provider=provider) | ||||||
| @ -68,7 +68,7 @@ class TestJWKS(OAuthTestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create(name="test", slug="test", provider=provider) |         app = Application.objects.create(name="test", slug="test", provider=provider) | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
| @ -82,7 +82,7 @@ class TestJWKS(OAuthTestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=create_test_cert(PrivateKeyAlg.ECDSA), |             signing_key=create_test_cert(PrivateKeyAlg.ECDSA), | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create(name="test", slug="test", provider=provider) |         app = Application.objects.create(name="test", slug="test", provider=provider) | ||||||
| @ -104,7 +104,7 @@ class TestJWKS(OAuthTestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=cert, |             signing_key=cert, | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create(name="test", slug="test", provider=provider) |         app = Application.objects.create(name="test", slug="test", provider=provider) | ||||||
|  | |||||||
| @ -10,14 +10,7 @@ from django.utils import timezone | |||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken | ||||||
|     AccessToken, |  | ||||||
|     IDToken, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     RefreshToken, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -29,7 +22,7 @@ class TesOAuth2Revoke(OAuthTestCase): | |||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.app = Application.objects.create( |         self.app = Application.objects.create( | ||||||
|  | |||||||
| @ -22,8 +22,6 @@ from authentik.providers.oauth2.models import ( | |||||||
|     AccessToken, |     AccessToken, | ||||||
|     AuthorizationCode, |     AuthorizationCode, | ||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     RefreshToken, |     RefreshToken, | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| ) | ) | ||||||
| @ -44,7 +42,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")], |             redirect_uris="http://TestServer", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() |         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
| @ -71,7 +69,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() |         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
| @ -92,7 +90,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() |         header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() | ||||||
| @ -120,7 +118,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         # Needs to be assigned to an application for iss to be set |         # Needs to be assigned to an application for iss to be set | ||||||
| @ -160,7 +158,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -222,7 +220,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -280,7 +278,7 @@ class TestToken(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|  | |||||||
| @ -19,12 +19,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
|     TOKEN_TYPE, |     TOKEN_TYPE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
| from authentik.providers.oauth2.views.jwks import JWKSView | from authentik.providers.oauth2.views.jwks import JWKSView | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| @ -59,7 +54,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase): | |||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=self.cert, |             signing_key=self.cert, | ||||||
|         ) |         ) | ||||||
|         self.provider.jwks_sources.add(self.source) |         self.provider.jwks_sources.add(self.source) | ||||||
|  | |||||||
| @ -19,13 +19,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     TOKEN_TYPE, |     TOKEN_TYPE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.errors import TokenError | from authentik.providers.oauth2.errors import TokenError | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||||
|     AccessToken, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -39,7 +33,7 @@ class TestTokenClientCredentialsStandard(OAuthTestCase): | |||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) |         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||||
| @ -113,48 +107,6 @@ class TestTokenClientCredentialsStandard(OAuthTestCase): | |||||||
|             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, |             {"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]}, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def test_incorrect_scopes(self): |  | ||||||
|         """test scope that isn't configured""" |  | ||||||
|         response = self.client.post( |  | ||||||
|             reverse("authentik_providers_oauth2:token"), |  | ||||||
|             { |  | ||||||
|                 "grant_type": GRANT_TYPE_CLIENT_CREDENTIALS, |  | ||||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE} extra_scope", |  | ||||||
|                 "client_id": self.provider.client_id, |  | ||||||
|                 "client_secret": self.provider.client_secret, |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(response.status_code, 200) |  | ||||||
|         body = loads(response.content.decode()) |  | ||||||
|         self.assertEqual(body["token_type"], TOKEN_TYPE) |  | ||||||
|         token = AccessToken.objects.filter( |  | ||||||
|             provider=self.provider, token=body["access_token"] |  | ||||||
|         ).first() |  | ||||||
|         self.assertSetEqual( |  | ||||||
|             set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE} |  | ||||||
|         ) |  | ||||||
|         _, alg = self.provider.jwt_key |  | ||||||
|         jwt = decode( |  | ||||||
|             body["access_token"], |  | ||||||
|             key=self.provider.signing_key.public_key, |  | ||||||
|             algorithms=[alg], |  | ||||||
|             audience=self.provider.client_id, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             jwt["given_name"], "Autogenerated user from application test (client credentials)" |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") |  | ||||||
|         jwt = decode( |  | ||||||
|             body["id_token"], |  | ||||||
|             key=self.provider.signing_key.public_key, |  | ||||||
|             algorithms=[alg], |  | ||||||
|             audience=self.provider.client_id, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             jwt["given_name"], "Autogenerated user from application test (client credentials)" |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials") |  | ||||||
|  |  | ||||||
|     def test_successful(self): |     def test_successful(self): | ||||||
|         """test successful""" |         """test successful""" | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|  | |||||||
| @ -20,12 +20,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     TOKEN_TYPE, |     TOKEN_TYPE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.errors import TokenError | from authentik.providers.oauth2.errors import TokenError | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -39,7 +34,7 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase): | |||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) |         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||||
|  | |||||||
| @ -19,12 +19,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     TOKEN_TYPE, |     TOKEN_TYPE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.errors import TokenError | from authentik.providers.oauth2.errors import TokenError | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -38,7 +33,7 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase): | |||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) |         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||||
|  | |||||||
| @ -9,19 +9,8 @@ from authentik.blueprints.tests import apply_blueprint | |||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_code_fixed_length, generate_id | from authentik.lib.generators import generate_code_fixed_length, generate_id | ||||||
| from authentik.providers.oauth2.constants import ( | from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE | ||||||
|     GRANT_TYPE_DEVICE_CODE, | from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping | ||||||
|     SCOPE_OPENID, |  | ||||||
|     SCOPE_OPENID_EMAIL, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.models import ( |  | ||||||
|     AccessToken, |  | ||||||
|     DeviceToken, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -35,7 +24,7 @@ class TestTokenDeviceCode(OAuthTestCase): | |||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], |             redirect_uris="http://testserver", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) |         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||||
| @ -91,28 +80,3 @@ class TestTokenDeviceCode(OAuthTestCase): | |||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(res.status_code, 200) |         self.assertEqual(res.status_code, 200) | ||||||
|  |  | ||||||
|     def test_code_mismatched_scope(self): |  | ||||||
|         """Test code with user (mismatched scopes)""" |  | ||||||
|         device_token = DeviceToken.objects.create( |  | ||||||
|             provider=self.provider, |  | ||||||
|             user_code=generate_code_fixed_length(), |  | ||||||
|             device_code=generate_id(), |  | ||||||
|             user=self.user, |  | ||||||
|             scope=[SCOPE_OPENID, SCOPE_OPENID_EMAIL], |  | ||||||
|         ) |  | ||||||
|         res = self.client.post( |  | ||||||
|             reverse("authentik_providers_oauth2:token"), |  | ||||||
|             data={ |  | ||||||
|                 "client_id": self.provider.client_id, |  | ||||||
|                 "grant_type": GRANT_TYPE_DEVICE_CODE, |  | ||||||
|                 "device_code": device_token.device_code, |  | ||||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} invalid", |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(res.status_code, 200) |  | ||||||
|         body = loads(res.content) |  | ||||||
|         token = AccessToken.objects.filter( |  | ||||||
|             provider=self.provider, token=body["access_token"] |  | ||||||
|         ).first() |  | ||||||
|         self.assertSetEqual(set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL}) |  | ||||||
|  | |||||||
| @ -10,12 +10,7 @@ from authentik.core.models import Application | |||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE | from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider | ||||||
|     AuthorizationCode, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -35,7 +30,7 @@ class TestTokenPKCE(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], |             redirect_uris="foo://localhost", | ||||||
|             access_code_validity="seconds=100", |             access_code_validity="seconds=100", | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
| @ -98,7 +93,7 @@ class TestTokenPKCE(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], |             redirect_uris="foo://localhost", | ||||||
|             access_code_validity="seconds=100", |             access_code_validity="seconds=100", | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
| @ -159,7 +154,7 @@ class TestTokenPKCE(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], |             redirect_uris="foo://localhost", | ||||||
|             access_code_validity="seconds=100", |             access_code_validity="seconds=100", | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
| @ -215,7 +210,7 @@ class TestTokenPKCE(OAuthTestCase): | |||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], |             redirect_uris="foo://localhost", | ||||||
|             access_code_validity="seconds=100", |             access_code_validity="seconds=100", | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  | |||||||
| @ -11,14 +11,7 @@ from authentik.core.models import Application | |||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping | ||||||
|     AccessToken, |  | ||||||
|     IDToken, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -32,7 +25,7 @@ class TestUserinfo(OAuthTestCase): | |||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         self.provider.property_mappings.set(ScopeMapping.objects.all()) |         self.provider.property_mappings.set(ScopeMapping.objects.all()) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| from dataclasses import InitVar, dataclass, field | from dataclasses import InitVar, dataclass, field | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  | from hashlib import sha256 | ||||||
| from json import dumps | from json import dumps | ||||||
| from re import error as RegexError | from re import error as RegexError | ||||||
| from re import fullmatch | from re import fullmatch | ||||||
| @ -15,7 +16,7 @@ from django.utils import timezone | |||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Application, AuthenticatedSession | from authentik.core.models import Application | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.events.signals import get_login_event | from authentik.events.signals import get_login_event | ||||||
| from authentik.flows.challenge import ( | from authentik.flows.challenge import ( | ||||||
| @ -56,8 +57,6 @@ from authentik.providers.oauth2.models import ( | |||||||
|     AuthorizationCode, |     AuthorizationCode, | ||||||
|     GrantTypes, |     GrantTypes, | ||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ResponseMode, |     ResponseMode, | ||||||
|     ResponseTypes, |     ResponseTypes, | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| @ -189,39 +188,40 @@ class OAuthAuthorizationParams: | |||||||
|  |  | ||||||
|     def check_redirect_uri(self): |     def check_redirect_uri(self): | ||||||
|         """Redirect URI validation.""" |         """Redirect URI validation.""" | ||||||
|         allowed_redirect_urls = self.provider.redirect_uris |         allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|         if not self.redirect_uri: |         if not self.redirect_uri: | ||||||
|             LOGGER.warning("Missing redirect uri.") |             LOGGER.warning("Missing redirect uri.") | ||||||
|             raise RedirectUriError("", allowed_redirect_urls) |             raise RedirectUriError("", allowed_redirect_urls) | ||||||
|  |  | ||||||
|         if len(allowed_redirect_urls) < 1: |         if self.provider.redirect_uris == "": | ||||||
|             LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri) |             LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri) | ||||||
|             self.provider.redirect_uris = [ |             self.provider.redirect_uris = self.redirect_uri | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, self.redirect_uri) |  | ||||||
|             ] |  | ||||||
|             self.provider.save() |             self.provider.save() | ||||||
|             allowed_redirect_urls = self.provider.redirect_uris |             allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|  |  | ||||||
|  |         if self.provider.redirect_uris == "*": | ||||||
|  |             LOGGER.info("Converting redirect_uris to regex", redirect=self.redirect_uri) | ||||||
|  |             self.provider.redirect_uris = ".*" | ||||||
|  |             self.provider.save() | ||||||
|  |             allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|  |  | ||||||
|         match_found = False |  | ||||||
|         for allowed in allowed_redirect_urls: |  | ||||||
|             if allowed.matching_mode == RedirectURIMatchingMode.STRICT: |  | ||||||
|                 if self.redirect_uri == allowed.url: |  | ||||||
|                     match_found = True |  | ||||||
|                     break |  | ||||||
|             if allowed.matching_mode == RedirectURIMatchingMode.REGEX: |  | ||||||
|         try: |         try: | ||||||
|                     if fullmatch(allowed.url, self.redirect_uri): |             if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls): | ||||||
|                         match_found = True |  | ||||||
|                         break |  | ||||||
|                 except RegexError as exc: |  | ||||||
|                 LOGGER.warning( |                 LOGGER.warning( | ||||||
|                         "Failed to parse regular expression", |                     "Invalid redirect uri (regex comparison)", | ||||||
|                         exc=exc, |                     redirect_uri_given=self.redirect_uri, | ||||||
|                         url=allowed.url, |                     redirect_uri_expected=allowed_redirect_urls, | ||||||
|                         provider=self.provider, |  | ||||||
|                 ) |                 ) | ||||||
|         if not match_found: |  | ||||||
|                 raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) |                 raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) | ||||||
|  |         except RegexError as exc: | ||||||
|  |             LOGGER.info("Failed to parse regular expression, checking directly", exc=exc) | ||||||
|  |             if not any(x == self.redirect_uri for x in allowed_redirect_urls): | ||||||
|  |                 LOGGER.warning( | ||||||
|  |                     "Invalid redirect uri (strict comparison)", | ||||||
|  |                     redirect_uri_given=self.redirect_uri, | ||||||
|  |                     redirect_uri_expected=allowed_redirect_urls, | ||||||
|  |                 ) | ||||||
|  |                 raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) from None | ||||||
|         # Check against forbidden schemes |         # Check against forbidden schemes | ||||||
|         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: |         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: | ||||||
|             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) |             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) | ||||||
| @ -318,9 +318,7 @@ class OAuthAuthorizationParams: | |||||||
|             expires=now + timedelta_from_string(self.provider.access_code_validity), |             expires=now + timedelta_from_string(self.provider.access_code_validity), | ||||||
|             scope=self.scope, |             scope=self.scope, | ||||||
|             nonce=self.nonce, |             nonce=self.nonce, | ||||||
|             session=AuthenticatedSession.objects.filter( |             session_id=sha256(request.session.session_key.encode("ascii")).hexdigest(), | ||||||
|                 session_key=request.session.session_key |  | ||||||
|             ).first(), |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         if self.code_challenge and self.code_challenge_method: |         if self.code_challenge and self.code_challenge_method: | ||||||
| @ -612,9 +610,7 @@ class OAuthFulfillmentStage(StageView): | |||||||
|             expires=access_token_expiry, |             expires=access_token_expiry, | ||||||
|             provider=self.provider, |             provider=self.provider, | ||||||
|             auth_time=auth_event.created if auth_event else now, |             auth_time=auth_event.created if auth_event else now, | ||||||
|             session=AuthenticatedSession.objects.filter( |             session_id=sha256(self.request.session.session_key.encode("ascii")).hexdigest(), | ||||||
|                 session_key=self.request.session.session_key |  | ||||||
|             ).first(), |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         id_token = IDToken.new(self.provider, token, self.request) |         id_token = IDToken.new(self.provider, token, self.request) | ||||||
|  | |||||||
| @ -158,5 +158,5 @@ class ProviderInfoView(View): | |||||||
|             OAuth2Provider, pk=application.provider_id |             OAuth2Provider, pk=application.provider_id | ||||||
|         ) |         ) | ||||||
|         response = super().dispatch(request, *args, **kwargs) |         response = super().dispatch(request, *args, **kwargs) | ||||||
|         cors_allow(request, response, *[x.url for x in self.provider.redirect_uris]) |         cors_allow(request, response, *self.provider.redirect_uris.split("\n")) | ||||||
|         return response |         return response | ||||||
|  | |||||||
| @ -58,9 +58,7 @@ from authentik.providers.oauth2.models import ( | |||||||
|     ClientTypes, |     ClientTypes, | ||||||
|     DeviceToken, |     DeviceToken, | ||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     RefreshToken, |     RefreshToken, | ||||||
|     ScopeMapping, |  | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth | from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth | ||||||
| from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES | from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES | ||||||
| @ -79,7 +77,7 @@ class TokenParams: | |||||||
|     redirect_uri: str |     redirect_uri: str | ||||||
|     grant_type: str |     grant_type: str | ||||||
|     state: str |     state: str | ||||||
|     scope: set[str] |     scope: list[str] | ||||||
|  |  | ||||||
|     provider: OAuth2Provider |     provider: OAuth2Provider | ||||||
|  |  | ||||||
| @ -114,26 +112,11 @@ class TokenParams: | |||||||
|             redirect_uri=request.POST.get("redirect_uri", ""), |             redirect_uri=request.POST.get("redirect_uri", ""), | ||||||
|             grant_type=request.POST.get("grant_type", ""), |             grant_type=request.POST.get("grant_type", ""), | ||||||
|             state=request.POST.get("state", ""), |             state=request.POST.get("state", ""), | ||||||
|             scope=set(request.POST.get("scope", "").split()), |             scope=request.POST.get("scope", "").split(), | ||||||
|             # PKCE parameter. |             # PKCE parameter. | ||||||
|             code_verifier=request.POST.get("code_verifier"), |             code_verifier=request.POST.get("code_verifier"), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def __check_scopes(self): |  | ||||||
|         allowed_scope_names = set( |  | ||||||
|             ScopeMapping.objects.filter(provider__in=[self.provider]).values_list( |  | ||||||
|                 "scope_name", flat=True |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         scopes_to_check = self.scope |  | ||||||
|         if not scopes_to_check.issubset(allowed_scope_names): |  | ||||||
|             LOGGER.info( |  | ||||||
|                 "Application requested scopes not configured, setting to overlap", |  | ||||||
|                 scope_allowed=allowed_scope_names, |  | ||||||
|                 scope_given=self.scope, |  | ||||||
|             ) |  | ||||||
|             self.scope = self.scope.intersection(allowed_scope_names) |  | ||||||
|  |  | ||||||
|     def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs): |     def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs): | ||||||
|         with start_span( |         with start_span( | ||||||
|             op="authentik.providers.oauth2.token.policy", |             op="authentik.providers.oauth2.token.policy", | ||||||
| @ -166,7 +149,7 @@ class TokenParams: | |||||||
|                     client_id=self.provider.client_id, |                     client_id=self.provider.client_id, | ||||||
|                 ) |                 ) | ||||||
|                 raise TokenError("invalid_client") |                 raise TokenError("invalid_client") | ||||||
|         self.__check_scopes() |  | ||||||
|         if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: |         if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: | ||||||
|             with start_span( |             with start_span( | ||||||
|                 op="authentik.providers.oauth2.post.parse.code", |                 op="authentik.providers.oauth2.post.parse.code", | ||||||
| @ -196,7 +179,42 @@ class TokenParams: | |||||||
|             LOGGER.warning("Missing authorization code") |             LOGGER.warning("Missing authorization code") | ||||||
|             raise TokenError("invalid_grant") |             raise TokenError("invalid_grant") | ||||||
|  |  | ||||||
|         self.__check_redirect_uri(request) |         allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|  |         # At this point, no provider should have a blank redirect_uri, in case they do | ||||||
|  |         # this will check an empty array and raise an error | ||||||
|  |         try: | ||||||
|  |             if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls): | ||||||
|  |                 LOGGER.warning( | ||||||
|  |                     "Invalid redirect uri (regex comparison)", | ||||||
|  |                     redirect_uri=self.redirect_uri, | ||||||
|  |                     expected=allowed_redirect_urls, | ||||||
|  |                 ) | ||||||
|  |                 Event.new( | ||||||
|  |                     EventAction.CONFIGURATION_ERROR, | ||||||
|  |                     message="Invalid redirect URI used by provider", | ||||||
|  |                     provider=self.provider, | ||||||
|  |                     redirect_uri=self.redirect_uri, | ||||||
|  |                     expected=allowed_redirect_urls, | ||||||
|  |                 ).from_http(request) | ||||||
|  |                 raise TokenError("invalid_client") | ||||||
|  |         except RegexError as exc: | ||||||
|  |             LOGGER.info("Failed to parse regular expression, checking directly", exc=exc) | ||||||
|  |             if not any(x == self.redirect_uri for x in allowed_redirect_urls): | ||||||
|  |                 LOGGER.warning( | ||||||
|  |                     "Invalid redirect uri (strict comparison)", | ||||||
|  |                     redirect_uri=self.redirect_uri, | ||||||
|  |                     expected=allowed_redirect_urls, | ||||||
|  |                 ) | ||||||
|  |                 Event.new( | ||||||
|  |                     EventAction.CONFIGURATION_ERROR, | ||||||
|  |                     message="Invalid redirect_uri configured", | ||||||
|  |                     provider=self.provider, | ||||||
|  |                 ).from_http(request) | ||||||
|  |                 raise TokenError("invalid_client") from None | ||||||
|  |  | ||||||
|  |         # Check against forbidden schemes | ||||||
|  |         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: | ||||||
|  |             raise TokenError("invalid_request") | ||||||
|  |  | ||||||
|         self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first() |         self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first() | ||||||
|         if not self.authorization_code: |         if not self.authorization_code: | ||||||
| @ -236,48 +254,6 @@ class TokenParams: | |||||||
|         if not self.authorization_code.code_challenge and self.code_verifier: |         if not self.authorization_code.code_challenge and self.code_verifier: | ||||||
|             raise TokenError("invalid_grant") |             raise TokenError("invalid_grant") | ||||||
|  |  | ||||||
|     def __check_redirect_uri(self, request: HttpRequest): |  | ||||||
|         allowed_redirect_urls = self.provider.redirect_uris |  | ||||||
|         # At this point, no provider should have a blank redirect_uri, in case they do |  | ||||||
|         # this will check an empty array and raise an error |  | ||||||
|  |  | ||||||
|         match_found = False |  | ||||||
|         for allowed in allowed_redirect_urls: |  | ||||||
|             if allowed.matching_mode == RedirectURIMatchingMode.STRICT: |  | ||||||
|                 if self.redirect_uri == allowed.url: |  | ||||||
|                     match_found = True |  | ||||||
|                     break |  | ||||||
|             if allowed.matching_mode == RedirectURIMatchingMode.REGEX: |  | ||||||
|                 try: |  | ||||||
|                     if fullmatch(allowed.url, self.redirect_uri): |  | ||||||
|                         match_found = True |  | ||||||
|                         break |  | ||||||
|                 except RegexError as exc: |  | ||||||
|                     LOGGER.warning( |  | ||||||
|                         "Failed to parse regular expression", |  | ||||||
|                         exc=exc, |  | ||||||
|                         url=allowed.url, |  | ||||||
|                         provider=self.provider, |  | ||||||
|                     ) |  | ||||||
|                     Event.new( |  | ||||||
|                         EventAction.CONFIGURATION_ERROR, |  | ||||||
|                         message="Invalid redirect_uri configured", |  | ||||||
|                         provider=self.provider, |  | ||||||
|                     ).from_http(request) |  | ||||||
|         if not match_found: |  | ||||||
|             Event.new( |  | ||||||
|                 EventAction.CONFIGURATION_ERROR, |  | ||||||
|                 message="Invalid redirect URI used by provider", |  | ||||||
|                 provider=self.provider, |  | ||||||
|                 redirect_uri=self.redirect_uri, |  | ||||||
|                 expected=allowed_redirect_urls, |  | ||||||
|             ).from_http(request) |  | ||||||
|             raise TokenError("invalid_client") |  | ||||||
|  |  | ||||||
|         # Check against forbidden schemes |  | ||||||
|         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: |  | ||||||
|             raise TokenError("invalid_request") |  | ||||||
|  |  | ||||||
|     def __post_init_refresh(self, raw_token: str, request: HttpRequest): |     def __post_init_refresh(self, raw_token: str, request: HttpRequest): | ||||||
|         if not raw_token: |         if not raw_token: | ||||||
|             LOGGER.warning("Missing refresh token") |             LOGGER.warning("Missing refresh token") | ||||||
| @ -463,14 +439,15 @@ class TokenParams: | |||||||
|                 # (22 chars being the length of the "template") |                 # (22 chars being the length of the "template") | ||||||
|                 username=f"ak-{self.provider.name[:150-22]}-client_credentials", |                 username=f"ak-{self.provider.name[:150-22]}-client_credentials", | ||||||
|                 defaults={ |                 defaults={ | ||||||
|  |                     "attributes": { | ||||||
|  |                         USER_ATTRIBUTE_GENERATED: True, | ||||||
|  |                     }, | ||||||
|                     "last_login": timezone.now(), |                     "last_login": timezone.now(), | ||||||
|                     "name": f"Autogenerated user from application {app.name} (client credentials)", |                     "name": f"Autogenerated user from application {app.name} (client credentials)", | ||||||
|                     "path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}", |                     "path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}", | ||||||
|                     "type": UserTypes.SERVICE_ACCOUNT, |                     "type": UserTypes.SERVICE_ACCOUNT, | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             self.user.attributes[USER_ATTRIBUTE_GENERATED] = True |  | ||||||
|             self.user.save() |  | ||||||
|         self.__check_policy_access(app, request) |         self.__check_policy_access(app, request) | ||||||
|  |  | ||||||
|         Event.new( |         Event.new( | ||||||
| @ -494,6 +471,9 @@ class TokenParams: | |||||||
|             self.user, created = User.objects.update_or_create( |             self.user, created = User.objects.update_or_create( | ||||||
|                 username=f"{self.provider.name}-{token.get('sub')}", |                 username=f"{self.provider.name}-{token.get('sub')}", | ||||||
|                 defaults={ |                 defaults={ | ||||||
|  |                     "attributes": { | ||||||
|  |                         USER_ATTRIBUTE_GENERATED: True, | ||||||
|  |                     }, | ||||||
|                     "last_login": timezone.now(), |                     "last_login": timezone.now(), | ||||||
|                     "name": ( |                     "name": ( | ||||||
|                         f"Autogenerated user from application {app.name} (client credentials JWT)" |                         f"Autogenerated user from application {app.name} (client credentials JWT)" | ||||||
| @ -502,8 +482,6 @@ class TokenParams: | |||||||
|                     "type": UserTypes.SERVICE_ACCOUNT, |                     "type": UserTypes.SERVICE_ACCOUNT, | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             self.user.attributes[USER_ATTRIBUTE_GENERATED] = True |  | ||||||
|             self.user.save() |  | ||||||
|             exp = token.get("exp") |             exp = token.get("exp") | ||||||
|             if created and exp: |             if created and exp: | ||||||
|                 self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp |                 self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp | ||||||
| @ -521,7 +499,7 @@ class TokenView(View): | |||||||
|         response = super().dispatch(request, *args, **kwargs) |         response = super().dispatch(request, *args, **kwargs) | ||||||
|         allowed_origins = [] |         allowed_origins = [] | ||||||
|         if self.provider: |         if self.provider: | ||||||
|             allowed_origins = [x.url for x in self.provider.redirect_uris] |             allowed_origins = self.provider.redirect_uris.split("\n") | ||||||
|         cors_allow(self.request, response, *allowed_origins) |         cors_allow(self.request, response, *allowed_origins) | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
| @ -574,7 +552,7 @@ class TokenView(View): | |||||||
|             # Keep same scopes as previous token |             # Keep same scopes as previous token | ||||||
|             scope=self.params.authorization_code.scope, |             scope=self.params.authorization_code.scope, | ||||||
|             auth_time=self.params.authorization_code.auth_time, |             auth_time=self.params.authorization_code.auth_time, | ||||||
|             session=self.params.authorization_code.session, |             session_id=self.params.authorization_code.session_id, | ||||||
|         ) |         ) | ||||||
|         access_id_token = IDToken.new( |         access_id_token = IDToken.new( | ||||||
|             self.provider, |             self.provider, | ||||||
| @ -602,7 +580,7 @@ class TokenView(View): | |||||||
|                 expires=refresh_token_expiry, |                 expires=refresh_token_expiry, | ||||||
|                 provider=self.provider, |                 provider=self.provider, | ||||||
|                 auth_time=self.params.authorization_code.auth_time, |                 auth_time=self.params.authorization_code.auth_time, | ||||||
|                 session=self.params.authorization_code.session, |                 session_id=self.params.authorization_code.session_id, | ||||||
|             ) |             ) | ||||||
|             id_token = IDToken.new( |             id_token = IDToken.new( | ||||||
|                 self.provider, |                 self.provider, | ||||||
| @ -635,7 +613,7 @@ class TokenView(View): | |||||||
|             # Keep same scopes as previous token |             # Keep same scopes as previous token | ||||||
|             scope=self.params.refresh_token.scope, |             scope=self.params.refresh_token.scope, | ||||||
|             auth_time=self.params.refresh_token.auth_time, |             auth_time=self.params.refresh_token.auth_time, | ||||||
|             session=self.params.refresh_token.session, |             session_id=self.params.refresh_token.session_id, | ||||||
|         ) |         ) | ||||||
|         access_token.id_token = IDToken.new( |         access_token.id_token = IDToken.new( | ||||||
|             self.provider, |             self.provider, | ||||||
| @ -651,7 +629,7 @@ class TokenView(View): | |||||||
|             expires=refresh_token_expiry, |             expires=refresh_token_expiry, | ||||||
|             provider=self.provider, |             provider=self.provider, | ||||||
|             auth_time=self.params.refresh_token.auth_time, |             auth_time=self.params.refresh_token.auth_time, | ||||||
|             session=self.params.refresh_token.session, |             session_id=self.params.refresh_token.session_id, | ||||||
|         ) |         ) | ||||||
|         id_token = IDToken.new( |         id_token = IDToken.new( | ||||||
|             self.provider, |             self.provider, | ||||||
| @ -709,14 +687,13 @@ class TokenView(View): | |||||||
|             raise DeviceCodeError("authorization_pending") |             raise DeviceCodeError("authorization_pending") | ||||||
|         now = timezone.now() |         now = timezone.now() | ||||||
|         access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity) |         access_token_expiry = now + timedelta_from_string(self.provider.access_token_validity) | ||||||
|         auth_event = get_login_event(self.params.device_code.session) |         auth_event = get_login_event(self.request) | ||||||
|         access_token = AccessToken( |         access_token = AccessToken( | ||||||
|             provider=self.provider, |             provider=self.provider, | ||||||
|             user=self.params.device_code.user, |             user=self.params.device_code.user, | ||||||
|             expires=access_token_expiry, |             expires=access_token_expiry, | ||||||
|             scope=self.params.device_code.scope, |             scope=self.params.device_code.scope, | ||||||
|             auth_time=auth_event.created if auth_event else now, |             auth_time=auth_event.created if auth_event else now, | ||||||
|             session=self.params.device_code.session, |  | ||||||
|         ) |         ) | ||||||
|         access_token.id_token = IDToken.new( |         access_token.id_token = IDToken.new( | ||||||
|             self.provider, |             self.provider, | ||||||
| @ -734,7 +711,7 @@ class TokenView(View): | |||||||
|             "id_token": access_token.id_token.to_jwt(self.provider), |             "id_token": access_token.id_token.to_jwt(self.provider), | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if SCOPE_OFFLINE_ACCESS in self.params.device_code.scope: |         if SCOPE_OFFLINE_ACCESS in self.params.scope: | ||||||
|             refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity) |             refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity) | ||||||
|             refresh_token = RefreshToken( |             refresh_token = RefreshToken( | ||||||
|                 user=self.params.device_code.user, |                 user=self.params.device_code.user, | ||||||
|  | |||||||
| @ -108,7 +108,7 @@ class UserInfoView(View): | |||||||
|         response = super().dispatch(request, *args, **kwargs) |         response = super().dispatch(request, *args, **kwargs) | ||||||
|         allowed_origins = [] |         allowed_origins = [] | ||||||
|         if self.token: |         if self.token: | ||||||
|             allowed_origins = [x.url for x in self.token.provider.redirect_uris] |             allowed_origins = self.token.provider.redirect_uris.split("\n") | ||||||
|         cors_allow(self.request, response, *allowed_origins) |         cors_allow(self.request, response, *allowed_origins) | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ 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 ModelSerializer, PassiveSerializer | from authentik.core.api.utils import ModelSerializer, PassiveSerializer | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.providers.oauth2.api.providers import RedirectURISerializer |  | ||||||
| from authentik.providers.oauth2.models import ScopeMapping | from authentik.providers.oauth2.models import ScopeMapping | ||||||
| from authentik.providers.oauth2.views.provider import ProviderInfoView | from authentik.providers.oauth2.views.provider import ProviderInfoView | ||||||
| from authentik.providers.proxy.models import ProxyMode, ProxyProvider | from authentik.providers.proxy.models import ProxyMode, ProxyProvider | ||||||
| @ -40,7 +39,7 @@ class ProxyProviderSerializer(ProviderSerializer): | |||||||
|     """ProxyProvider Serializer""" |     """ProxyProvider Serializer""" | ||||||
|  |  | ||||||
|     client_id = CharField(read_only=True) |     client_id = CharField(read_only=True) | ||||||
|     redirect_uris = RedirectURISerializer(many=True, read_only=True, source="_redirect_uris") |     redirect_uris = CharField(read_only=True) | ||||||
|     outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all") |     outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all") | ||||||
|  |  | ||||||
|     def validate_basic_auth_enabled(self, value: bool) -> bool: |     def validate_basic_auth_enabled(self, value: bool) -> bool: | ||||||
| @ -122,6 +121,7 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet): | |||||||
|         "basic_auth_password_attribute": ["iexact"], |         "basic_auth_password_attribute": ["iexact"], | ||||||
|         "basic_auth_user_attribute": ["iexact"], |         "basic_auth_user_attribute": ["iexact"], | ||||||
|         "mode": ["iexact"], |         "mode": ["iexact"], | ||||||
|  |         "redirect_uris": ["iexact"], | ||||||
|         "cookie_domain": ["iexact"], |         "cookie_domain": ["iexact"], | ||||||
|     } |     } | ||||||
|     search_fields = ["name"] |     search_fields = ["name"] | ||||||
|  | |||||||
| @ -13,13 +13,7 @@ from rest_framework.serializers import Serializer | |||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.lib.models import DomainlessURLValidator | from authentik.lib.models import DomainlessURLValidator | ||||||
| from authentik.outposts.models import OutpostModel | from authentik.outposts.models import OutpostModel | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
|     ClientTypes, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| SCOPE_AK_PROXY = "ak_proxy" | SCOPE_AK_PROXY = "ak_proxy" | ||||||
| OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback" | OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback" | ||||||
| @ -30,14 +24,14 @@ def get_cookie_secret(): | |||||||
|     return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32)) |     return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _get_callback_url(uri: str) -> list[RedirectURI]: | def _get_callback_url(uri: str) -> str: | ||||||
|     return [ |     return "\n".join( | ||||||
|         RedirectURI( |         [ | ||||||
|             RedirectURIMatchingMode.STRICT, |             urljoin(uri, "outpost.goauthentik.io/callback") | ||||||
|             urljoin(uri, "outpost.goauthentik.io/callback") + f"?{OUTPOST_CALLBACK_SIGNATURE}=true", |             + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true", | ||||||
|         ), |             uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true", | ||||||
|         RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"?{OUTPOST_CALLBACK_SIGNATURE}=true"), |  | ||||||
|         ] |         ] | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProxyMode(models.TextChoices): | class ProxyMode(models.TextChoices): | ||||||
|  | |||||||
| @ -1,12 +1,13 @@ | |||||||
| """proxy provider tasks""" | """proxy provider tasks""" | ||||||
|  |  | ||||||
|  | from hashlib import sha256 | ||||||
|  |  | ||||||
| from asgiref.sync import async_to_sync | from asgiref.sync import async_to_sync | ||||||
| from channels.layers import get_channel_layer | from channels.layers import get_channel_layer | ||||||
| from django.db import DatabaseError, InternalError, ProgrammingError | from django.db import DatabaseError, InternalError, ProgrammingError | ||||||
|  |  | ||||||
| from authentik.outposts.consumer import OUTPOST_GROUP | from authentik.outposts.consumer import OUTPOST_GROUP | ||||||
| from authentik.outposts.models import Outpost, OutpostType | from authentik.outposts.models import Outpost, OutpostType | ||||||
| from authentik.providers.oauth2.id_token import hash_session_key |  | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
| @ -25,7 +26,7 @@ def proxy_set_defaults(): | |||||||
| def proxy_on_logout(session_id: str): | def proxy_on_logout(session_id: str): | ||||||
|     """Update outpost instances connected to a single outpost""" |     """Update outpost instances connected to a single outpost""" | ||||||
|     layer = get_channel_layer() |     layer = get_channel_layer() | ||||||
|     hashed_session_id = hash_session_key(session_id) |     hashed_session_id = sha256(session_id.encode("ascii")).hexdigest() | ||||||
|     for outpost in Outpost.objects.filter(type=OutpostType.PROXY): |     for outpost in Outpost.objects.filter(type=OutpostType.PROXY): | ||||||
|         group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)} |         group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)} | ||||||
|         async_to_sync(layer.group_send)( |         async_to_sync(layer.group_send)( | ||||||
|  | |||||||
| @ -50,7 +50,6 @@ class AssertionProcessor: | |||||||
|  |  | ||||||
|     _issue_instant: str |     _issue_instant: str | ||||||
|     _assertion_id: str |     _assertion_id: str | ||||||
|     _response_id: str |  | ||||||
|  |  | ||||||
|     _valid_not_before: str |     _valid_not_before: str | ||||||
|     _session_not_on_or_after: str |     _session_not_on_or_after: str | ||||||
| @ -63,7 +62,6 @@ class AssertionProcessor: | |||||||
|  |  | ||||||
|         self._issue_instant = get_time_string() |         self._issue_instant = get_time_string() | ||||||
|         self._assertion_id = get_random_id() |         self._assertion_id = get_random_id() | ||||||
|         self._response_id = get_random_id() |  | ||||||
|  |  | ||||||
|         self._valid_not_before = get_time_string( |         self._valid_not_before = get_time_string( | ||||||
|             timedelta_from_string(self.provider.assertion_valid_not_before) |             timedelta_from_string(self.provider.assertion_valid_not_before) | ||||||
| @ -132,9 +130,7 @@ class AssertionProcessor: | |||||||
|         """Generate AuthnStatement with AuthnContext and ContextClassRef Elements.""" |         """Generate AuthnStatement with AuthnContext and ContextClassRef Elements.""" | ||||||
|         auth_n_statement = Element(f"{{{NS_SAML_ASSERTION}}}AuthnStatement") |         auth_n_statement = Element(f"{{{NS_SAML_ASSERTION}}}AuthnStatement") | ||||||
|         auth_n_statement.attrib["AuthnInstant"] = self._valid_not_before |         auth_n_statement.attrib["AuthnInstant"] = self._valid_not_before | ||||||
|         auth_n_statement.attrib["SessionIndex"] = sha256( |         auth_n_statement.attrib["SessionIndex"] = self._assertion_id | ||||||
|             self.http_request.session.session_key.encode("ascii") |  | ||||||
|         ).hexdigest() |  | ||||||
|         auth_n_statement.attrib["SessionNotOnOrAfter"] = self._session_not_on_or_after |         auth_n_statement.attrib["SessionNotOnOrAfter"] = self._session_not_on_or_after | ||||||
|  |  | ||||||
|         auth_n_context = SubElement(auth_n_statement, f"{{{NS_SAML_ASSERTION}}}AuthnContext") |         auth_n_context = SubElement(auth_n_statement, f"{{{NS_SAML_ASSERTION}}}AuthnContext") | ||||||
| @ -289,7 +285,7 @@ class AssertionProcessor: | |||||||
|         response.attrib["Version"] = "2.0" |         response.attrib["Version"] = "2.0" | ||||||
|         response.attrib["IssueInstant"] = self._issue_instant |         response.attrib["IssueInstant"] = self._issue_instant | ||||||
|         response.attrib["Destination"] = self.provider.acs_url |         response.attrib["Destination"] = self.provider.acs_url | ||||||
|         response.attrib["ID"] = self._response_id |         response.attrib["ID"] = get_random_id() | ||||||
|         if self.auth_n_request.id: |         if self.auth_n_request.id: | ||||||
|             response.attrib["InResponseTo"] = self.auth_n_request.id |             response.attrib["InResponseTo"] = self.auth_n_request.id | ||||||
|  |  | ||||||
| @ -312,7 +308,7 @@ class AssertionProcessor: | |||||||
|         ref = xmlsec.template.add_reference( |         ref = xmlsec.template.add_reference( | ||||||
|             signature_node, |             signature_node, | ||||||
|             digest_algorithm_transform, |             digest_algorithm_transform, | ||||||
|             uri="#" + element.attrib["ID"], |             uri="#" + self._assertion_id, | ||||||
|         ) |         ) | ||||||
|         xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped) |         xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped) | ||||||
|         xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N) |         xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N) | ||||||
|  | |||||||
| @ -180,10 +180,6 @@ class TestAuthNRequest(TestCase): | |||||||
|         # Now create a response and convert it to string (provider) |         # Now create a response and convert it to string (provider) | ||||||
|         response_proc = AssertionProcessor(self.provider, http_request, parsed_request) |         response_proc = AssertionProcessor(self.provider, http_request, parsed_request) | ||||||
|         response = response_proc.build_response() |         response = response_proc.build_response() | ||||||
|         # Ensure both response and assertion ID are in the response twice (once as ID attribute, |  | ||||||
|         # once as ds:Reference URI) |  | ||||||
|         self.assertEqual(response.count(response_proc._assertion_id), 2) |  | ||||||
|         self.assertEqual(response.count(response_proc._response_id), 2) |  | ||||||
|  |  | ||||||
|         # Now parse the response (source) |         # Now parse the response (source) | ||||||
|         http_request.POST = QueryDict(mutable=True) |         http_request.POST = QueryDict(mutable=True) | ||||||
|  | |||||||
| @ -2,10 +2,9 @@ | |||||||
|  |  | ||||||
| from itertools import batched | from itertools import batched | ||||||
|  |  | ||||||
| from django.db import transaction |  | ||||||
| from pydantic import ValidationError | from pydantic import ValidationError | ||||||
| from pydanticscim.group import GroupMember | from pydanticscim.group import GroupMember | ||||||
| from pydanticscim.responses import PatchOp | from pydanticscim.responses import PatchOp, PatchOperation | ||||||
|  |  | ||||||
| from authentik.core.models import Group | from authentik.core.models import Group | ||||||
| from authentik.lib.sync.mapper import PropertyMappingManager | from authentik.lib.sync.mapper import PropertyMappingManager | ||||||
| @ -20,7 +19,7 @@ from authentik.providers.scim.clients.base import SCIMClient | |||||||
| from authentik.providers.scim.clients.exceptions import ( | from authentik.providers.scim.clients.exceptions import ( | ||||||
|     SCIMRequestException, |     SCIMRequestException, | ||||||
| ) | ) | ||||||
| from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOperation, PatchRequest | from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchRequest | ||||||
| from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema | from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema | ||||||
| from authentik.providers.scim.models import ( | from authentik.providers.scim.models import ( | ||||||
|     SCIMMapping, |     SCIMMapping, | ||||||
| @ -105,47 +104,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|             provider=self.provider, group=group, scim_id=scim_id |             provider=self.provider, group=group, scim_id=scim_id | ||||||
|         ) |         ) | ||||||
|         users = list(group.users.order_by("id").values_list("id", flat=True)) |         users = list(group.users.order_by("id").values_list("id", flat=True)) | ||||||
|         self._patch_add_users(connection, users) |         self._patch_add_users(group, users) | ||||||
|         return connection |         return connection | ||||||
|  |  | ||||||
|     def update(self, group: Group, connection: SCIMProviderGroup): |     def update(self, group: Group, connection: SCIMProviderGroup): | ||||||
|         """Update existing group""" |         """Update existing group""" | ||||||
|         scim_group = self.to_schema(group, connection) |         scim_group = self.to_schema(group, connection) | ||||||
|         scim_group.id = connection.scim_id |         scim_group.id = connection.scim_id | ||||||
|         try: |  | ||||||
|             if self._config.patch.supported: |  | ||||||
|                 return self._update_patch(group, scim_group, connection) |  | ||||||
|             return self._update_put(group, scim_group, connection) |  | ||||||
|         except NotFoundSyncException: |  | ||||||
|             # Resource missing is handled by self.write, which will re-create the group |  | ||||||
|             raise |  | ||||||
|  |  | ||||||
|     def _update_patch( |  | ||||||
|         self, group: Group, scim_group: SCIMGroupSchema, connection: SCIMProviderGroup |  | ||||||
|     ): |  | ||||||
|         """Update a group via PATCH request""" |  | ||||||
|         # Patch group's attributes instead of replacing it and re-adding users if we can |  | ||||||
|         self._request( |  | ||||||
|             "PATCH", |  | ||||||
|             f"/Groups/{connection.scim_id}", |  | ||||||
|             json=PatchRequest( |  | ||||||
|                 Operations=[ |  | ||||||
|                     PatchOperation( |  | ||||||
|                         op=PatchOp.replace, |  | ||||||
|                         path=None, |  | ||||||
|                         value=scim_group.model_dump(mode="json", exclude_unset=True), |  | ||||||
|                     ) |  | ||||||
|                 ] |  | ||||||
|             ).model_dump( |  | ||||||
|                 mode="json", |  | ||||||
|                 exclude_unset=True, |  | ||||||
|                 exclude_none=True, |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|         return self.patch_compare_users(group) |  | ||||||
|  |  | ||||||
|     def _update_put(self, group: Group, scim_group: SCIMGroupSchema, connection: SCIMProviderGroup): |  | ||||||
|         """Update a group via PUT request""" |  | ||||||
|         try: |         try: | ||||||
|             self._request( |             self._request( | ||||||
|                 "PUT", |                 "PUT", | ||||||
| @ -155,25 +120,33 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|                     exclude_unset=True, |                     exclude_unset=True, | ||||||
|                 ), |                 ), | ||||||
|             ) |             ) | ||||||
|             return self.patch_compare_users(group) |             users = list(group.users.order_by("id").values_list("id", flat=True)) | ||||||
|  |             return self._patch_add_users(group, users) | ||||||
|  |         except NotFoundSyncException: | ||||||
|  |             # Resource missing is handled by self.write, which will re-create the group | ||||||
|  |             raise | ||||||
|         except (SCIMRequestException, ObjectExistsSyncException): |         except (SCIMRequestException, ObjectExistsSyncException): | ||||||
|             # Some providers don't support PUT on groups, so this is mainly a fix for the initial |             # Some providers don't support PUT on groups, so this is mainly a fix for the initial | ||||||
|             # sync, send patch add requests for all the users the group currently has |             # sync, send patch add requests for all the users the group currently has | ||||||
|             return self._update_patch(group, scim_group, connection) |             users = list(group.users.order_by("id").values_list("id", flat=True)) | ||||||
|  |             self._patch_add_users(group, users) | ||||||
|  |             # Also update the group name | ||||||
|  |             return self._patch( | ||||||
|  |                 scim_group.id, | ||||||
|  |                 PatchOperation( | ||||||
|  |                     op=PatchOp.replace, | ||||||
|  |                     path="displayName", | ||||||
|  |                     value=scim_group.displayName, | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     def update_group(self, group: Group, action: Direction, users_set: set[int]): |     def update_group(self, group: Group, action: Direction, users_set: set[int]): | ||||||
|         """Update a group, either using PUT to replace it or PATCH if supported""" |         """Update a group, either using PUT to replace it or PATCH if supported""" | ||||||
|         scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first() |  | ||||||
|         if not scim_group: |  | ||||||
|             self.logger.warning( |  | ||||||
|                 "could not sync group membership, group does not exist", group=group |  | ||||||
|             ) |  | ||||||
|             return |  | ||||||
|         if self._config.patch.supported: |         if self._config.patch.supported: | ||||||
|             if action == Direction.add: |             if action == Direction.add: | ||||||
|                 return self._patch_add_users(scim_group, users_set) |                 return self._patch_add_users(group, users_set) | ||||||
|             if action == Direction.remove: |             if action == Direction.remove: | ||||||
|                 return self._patch_remove_users(scim_group, users_set) |                 return self._patch_remove_users(group, users_set) | ||||||
|         try: |         try: | ||||||
|             return self.write(group) |             return self.write(group) | ||||||
|         except SCIMRequestException as exc: |         except SCIMRequestException as exc: | ||||||
| @ -181,24 +154,19 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|                 # Assume that provider does not support PUT and also doesn't support |                 # Assume that provider does not support PUT and also doesn't support | ||||||
|                 # ServiceProviderConfig, so try PATCH as a fallback |                 # ServiceProviderConfig, so try PATCH as a fallback | ||||||
|                 if action == Direction.add: |                 if action == Direction.add: | ||||||
|                     return self._patch_add_users(scim_group, users_set) |                     return self._patch_add_users(group, users_set) | ||||||
|                 if action == Direction.remove: |                 if action == Direction.remove: | ||||||
|                     return self._patch_remove_users(scim_group, users_set) |                     return self._patch_remove_users(group, users_set) | ||||||
|             raise exc |             raise exc | ||||||
|  |  | ||||||
|     def _patch_chunked( |     def _patch( | ||||||
|         self, |         self, | ||||||
|         group_id: str, |         group_id: str, | ||||||
|         *ops: PatchOperation, |         *ops: PatchOperation, | ||||||
|     ): |     ): | ||||||
|         """Helper function that chunks patch requests based on the maxOperations attribute. |  | ||||||
|         This is not strictly according to specs but there's nothing in the schema that allows the |  | ||||||
|         us to know what the maximum patch operations per request should be.""" |  | ||||||
|         chunk_size = self._config.bulk.maxOperations |         chunk_size = self._config.bulk.maxOperations | ||||||
|         if chunk_size < 1: |         if chunk_size < 1: | ||||||
|             chunk_size = len(ops) |             chunk_size = len(ops) | ||||||
|         if len(ops) < 1: |  | ||||||
|             return |  | ||||||
|         for chunk in batched(ops, chunk_size): |         for chunk in batched(ops, chunk_size): | ||||||
|             req = PatchRequest(Operations=list(chunk)) |             req = PatchRequest(Operations=list(chunk)) | ||||||
|             self._request( |             self._request( | ||||||
| @ -209,70 +177,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|                 ), |                 ), | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     @transaction.atomic |     def _patch_add_users(self, group: Group, users_set: set[int]): | ||||||
|     def patch_compare_users(self, group: Group): |         """Add users in users_set to group""" | ||||||
|         """Compare users with a SCIM group and add/remove any differences""" |         if len(users_set) < 1: | ||||||
|         # Get scim group first |             return | ||||||
|         scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first() |         scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first() | ||||||
|         if not scim_group: |         if not scim_group: | ||||||
|             self.logger.warning( |             self.logger.warning( | ||||||
|                 "could not sync group membership, group does not exist", group=group |                 "could not sync group membership, group does not exist", group=group | ||||||
|             ) |             ) | ||||||
|             return |             return | ||||||
|         # Get a list of all users in the authentik group |  | ||||||
|         raw_users_should = list(group.users.order_by("id").values_list("id", flat=True)) |  | ||||||
|         # Lookup the SCIM IDs of the users |  | ||||||
|         users_should: list[str] = list( |  | ||||||
|             SCIMProviderUser.objects.filter( |  | ||||||
|                 user__pk__in=raw_users_should, provider=self.provider |  | ||||||
|             ).values_list("scim_id", flat=True) |  | ||||||
|         ) |  | ||||||
|         if len(raw_users_should) != len(users_should): |  | ||||||
|             self.logger.warning( |  | ||||||
|                 "User count mismatch, not all users in the group are synced to SCIM yet.", |  | ||||||
|                 group=group, |  | ||||||
|             ) |  | ||||||
|         # Get current group status |  | ||||||
|         current_group = SCIMGroupSchema.model_validate( |  | ||||||
|             self._request("GET", f"/Groups/{scim_group.scim_id}") |  | ||||||
|         ) |  | ||||||
|         users_to_add = [] |  | ||||||
|         users_to_remove = [] |  | ||||||
|         # Check users currently in group and if they shouldn't be in the group and remove them |  | ||||||
|         for user in current_group.members or []: |  | ||||||
|             if user.value not in users_should: |  | ||||||
|                 users_to_remove.append(user.value) |  | ||||||
|         # Check users that should be in the group and add them |  | ||||||
|         for user in users_should: |  | ||||||
|             if len([x for x in current_group.members if x.value == user]) < 1: |  | ||||||
|                 users_to_add.append(user) |  | ||||||
|         # Only send request if we need to make changes |  | ||||||
|         if len(users_to_add) < 1 and len(users_to_remove) < 1: |  | ||||||
|             return |  | ||||||
|         return self._patch_chunked( |  | ||||||
|             scim_group.scim_id, |  | ||||||
|             *[ |  | ||||||
|                 PatchOperation( |  | ||||||
|                     op=PatchOp.add, |  | ||||||
|                     path="members", |  | ||||||
|                     value=[{"value": x}], |  | ||||||
|                 ) |  | ||||||
|                 for x in users_to_add |  | ||||||
|             ], |  | ||||||
|             *[ |  | ||||||
|                 PatchOperation( |  | ||||||
|                     op=PatchOp.remove, |  | ||||||
|                     path="members", |  | ||||||
|                     value=[{"value": x}], |  | ||||||
|                 ) |  | ||||||
|                 for x in users_to_remove |  | ||||||
|             ], |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def _patch_add_users(self, scim_group: SCIMProviderGroup, users_set: set[int]): |  | ||||||
|         """Add users in users_set to group""" |  | ||||||
|         if len(users_set) < 1: |  | ||||||
|             return |  | ||||||
|         user_ids = list( |         user_ids = list( | ||||||
|             SCIMProviderUser.objects.filter( |             SCIMProviderUser.objects.filter( | ||||||
|                 user__pk__in=users_set, provider=self.provider |                 user__pk__in=users_set, provider=self.provider | ||||||
| @ -280,7 +194,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|         ) |         ) | ||||||
|         if len(user_ids) < 1: |         if len(user_ids) < 1: | ||||||
|             return |             return | ||||||
|         self._patch_chunked( |         self._patch( | ||||||
|             scim_group.scim_id, |             scim_group.scim_id, | ||||||
|             *[ |             *[ | ||||||
|                 PatchOperation( |                 PatchOperation( | ||||||
| @ -292,10 +206,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def _patch_remove_users(self, scim_group: SCIMProviderGroup, users_set: set[int]): |     def _patch_remove_users(self, group: Group, users_set: set[int]): | ||||||
|         """Remove users in users_set from group""" |         """Remove users in users_set from group""" | ||||||
|         if len(users_set) < 1: |         if len(users_set) < 1: | ||||||
|             return |             return | ||||||
|  |         scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first() | ||||||
|  |         if not scim_group: | ||||||
|  |             self.logger.warning( | ||||||
|  |                 "could not sync group membership, group does not exist", group=group | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|         user_ids = list( |         user_ids = list( | ||||||
|             SCIMProviderUser.objects.filter( |             SCIMProviderUser.objects.filter( | ||||||
|                 user__pk__in=users_set, provider=self.provider |                 user__pk__in=users_set, provider=self.provider | ||||||
| @ -303,7 +223,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|         ) |         ) | ||||||
|         if len(user_ids) < 1: |         if len(user_ids) < 1: | ||||||
|             return |             return | ||||||
|         self._patch_chunked( |         self._patch( | ||||||
|             scim_group.scim_id, |             scim_group.scim_id, | ||||||
|             *[ |             *[ | ||||||
|                 PatchOperation( |                 PatchOperation( | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ | |||||||
|  |  | ||||||
| from pydantic import Field | from pydantic import Field | ||||||
| from pydanticscim.group import Group as BaseGroup | from pydanticscim.group import Group as BaseGroup | ||||||
| from pydanticscim.responses import PatchOperation as BasePatchOperation |  | ||||||
| from pydanticscim.responses import PatchRequest as BasePatchRequest | from pydanticscim.responses import PatchRequest as BasePatchRequest | ||||||
| from pydanticscim.responses import SCIMError as BaseSCIMError | from pydanticscim.responses import SCIMError as BaseSCIMError | ||||||
| from pydanticscim.service_provider import Bulk as BaseBulk | from pydanticscim.service_provider import Bulk as BaseBulk | ||||||
| @ -69,12 +68,6 @@ class PatchRequest(BasePatchRequest): | |||||||
|     schemas: tuple[str] = ("urn:ietf:params:scim:api:messages:2.0:PatchOp",) |     schemas: tuple[str] = ("urn:ietf:params:scim:api:messages:2.0:PatchOp",) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PatchOperation(BasePatchOperation): |  | ||||||
|     """PatchOperation with optional path""" |  | ||||||
|  |  | ||||||
|     path: str | None |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SCIMError(BaseSCIMError): | class SCIMError(BaseSCIMError): | ||||||
|     """SCIM error with optional status code""" |     """SCIM error with optional status code""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -252,118 +252,3 @@ class SCIMMembershipTests(TestCase): | |||||||
|                     ], |                     ], | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     def test_member_add_save(self): |  | ||||||
|         """Test member add + save""" |  | ||||||
|         config = ServiceProviderConfiguration.default() |  | ||||||
|  |  | ||||||
|         config.patch.supported = True |  | ||||||
|         user_scim_id = generate_id() |  | ||||||
|         group_scim_id = generate_id() |  | ||||||
|         uid = generate_id() |  | ||||||
|         group = Group.objects.create( |  | ||||||
|             name=uid, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         user = User.objects.create(username=generate_id()) |  | ||||||
|  |  | ||||||
|         # Test initial sync of group creation |  | ||||||
|         with Mocker() as mocker: |  | ||||||
|             mocker.get( |  | ||||||
|                 "https://localhost/ServiceProviderConfig", |  | ||||||
|                 json=config.model_dump(), |  | ||||||
|             ) |  | ||||||
|             mocker.post( |  | ||||||
|                 "https://localhost/Users", |  | ||||||
|                 json={ |  | ||||||
|                     "id": user_scim_id, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|             mocker.post( |  | ||||||
|                 "https://localhost/Groups", |  | ||||||
|                 json={ |  | ||||||
|                     "id": group_scim_id, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             self.configure() |  | ||||||
|             sync_tasks.trigger_single_task(self.provider, scim_sync).get() |  | ||||||
|  |  | ||||||
|             self.assertEqual(mocker.call_count, 6) |  | ||||||
|             self.assertEqual(mocker.request_history[0].method, "GET") |  | ||||||
|             self.assertEqual(mocker.request_history[1].method, "GET") |  | ||||||
|             self.assertEqual(mocker.request_history[2].method, "GET") |  | ||||||
|             self.assertEqual(mocker.request_history[3].method, "POST") |  | ||||||
|             self.assertEqual(mocker.request_history[4].method, "GET") |  | ||||||
|             self.assertEqual(mocker.request_history[5].method, "POST") |  | ||||||
|             self.assertJSONEqual( |  | ||||||
|                 mocker.request_history[3].body, |  | ||||||
|                 { |  | ||||||
|                     "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], |  | ||||||
|                     "emails": [], |  | ||||||
|                     "active": True, |  | ||||||
|                     "externalId": user.uid, |  | ||||||
|                     "name": {"familyName": " ", "formatted": " ", "givenName": ""}, |  | ||||||
|                     "displayName": "", |  | ||||||
|                     "userName": user.username, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|             self.assertJSONEqual( |  | ||||||
|                 mocker.request_history[5].body, |  | ||||||
|                 { |  | ||||||
|                     "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], |  | ||||||
|                     "externalId": str(group.pk), |  | ||||||
|                     "displayName": group.name, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         with Mocker() as mocker: |  | ||||||
|             mocker.get( |  | ||||||
|                 "https://localhost/ServiceProviderConfig", |  | ||||||
|                 json=config.model_dump(), |  | ||||||
|             ) |  | ||||||
|             mocker.get( |  | ||||||
|                 f"https://localhost/Groups/{group_scim_id}", |  | ||||||
|                 json={}, |  | ||||||
|             ) |  | ||||||
|             mocker.patch( |  | ||||||
|                 f"https://localhost/Groups/{group_scim_id}", |  | ||||||
|                 json={}, |  | ||||||
|             ) |  | ||||||
|             group.users.add(user) |  | ||||||
|             group.save() |  | ||||||
|             self.assertEqual(mocker.call_count, 5) |  | ||||||
|             self.assertEqual(mocker.request_history[0].method, "GET") |  | ||||||
|             self.assertEqual(mocker.request_history[1].method, "PATCH") |  | ||||||
|             self.assertEqual(mocker.request_history[2].method, "GET") |  | ||||||
|             self.assertEqual(mocker.request_history[3].method, "PATCH") |  | ||||||
|             self.assertEqual(mocker.request_history[4].method, "GET") |  | ||||||
|             self.assertJSONEqual( |  | ||||||
|                 mocker.request_history[1].body, |  | ||||||
|                 { |  | ||||||
|                     "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], |  | ||||||
|                     "Operations": [ |  | ||||||
|                         { |  | ||||||
|                             "op": "add", |  | ||||||
|                             "path": "members", |  | ||||||
|                             "value": [{"value": user_scim_id}], |  | ||||||
|                         } |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|             self.assertJSONEqual( |  | ||||||
|                 mocker.request_history[3].body, |  | ||||||
|                 { |  | ||||||
|                     "Operations": [ |  | ||||||
|                         { |  | ||||||
|                             "op": "replace", |  | ||||||
|                             "value": { |  | ||||||
|                                 "id": group_scim_id, |  | ||||||
|                                 "displayName": group.name, |  | ||||||
|                                 "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], |  | ||||||
|                                 "externalId": str(group.pk), |  | ||||||
|                             }, |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|  | |||||||
| @ -228,9 +228,9 @@ class ClientIPMiddleware: | |||||||
|             ) |             ) | ||||||
|             return None |             return None | ||||||
|         # Update sentry scope to include correct IP |         # Update sentry scope to include correct IP | ||||||
|         user = Scope.get_isolation_scope()._user or {} |         sentry_user = Scope.get_isolation_scope()._user or {} | ||||||
|         user["ip_address"] = delegated_ip |         sentry_user["ip_address"] = delegated_ip | ||||||
|         Scope.get_isolation_scope().set_user(user) |         Scope.get_isolation_scope().set_user(sentry_user) | ||||||
|         # Set the outpost service account on the request |         # Set the outpost service account on the request | ||||||
|         setattr(request, self.request_attr_outpost_user, user) |         setattr(request, self.request_attr_outpost_user, user) | ||||||
|         try: |         try: | ||||||
|  | |||||||
| @ -1,8 +1,6 @@ | |||||||
| """Metrics view""" | """Metrics view""" | ||||||
|  |  | ||||||
| from hmac import compare_digest | from base64 import b64encode | ||||||
| from pathlib import Path |  | ||||||
| from tempfile import gettempdir |  | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import connections | from django.db import connections | ||||||
| @ -18,21 +16,22 @@ monitoring_set = Signal() | |||||||
|  |  | ||||||
|  |  | ||||||
| class MetricsView(View): | class MetricsView(View): | ||||||
|     """Wrapper around ExportToDjangoView with authentication, accessed by the authentik router""" |     """Wrapper around ExportToDjangoView, using http-basic auth""" | ||||||
|  |  | ||||||
|     def __init__(self, **kwargs): |  | ||||||
|         _tmp = Path(gettempdir()) |  | ||||||
|         with open(_tmp / "authentik-core-metrics.key") as _f: |  | ||||||
|             self.monitoring_key = _f.read() |  | ||||||
|  |  | ||||||
|     def get(self, request: HttpRequest) -> HttpResponse: |     def get(self, request: HttpRequest) -> HttpResponse: | ||||||
|         """Check for HTTP-Basic auth""" |         """Check for HTTP-Basic auth""" | ||||||
|         auth_header = request.META.get("HTTP_AUTHORIZATION", "") |         auth_header = request.META.get("HTTP_AUTHORIZATION", "") | ||||||
|         auth_type, _, given_credentials = auth_header.partition(" ") |         auth_type, _, given_credentials = auth_header.partition(" ") | ||||||
|         authed = auth_type == "Bearer" and compare_digest(given_credentials, self.monitoring_key) |         credentials = f"monitor:{settings.SECRET_KEY}" | ||||||
|  |         expected = b64encode(str.encode(credentials)).decode() | ||||||
|  |         authed = auth_type == "Basic" and given_credentials == expected | ||||||
|         if not authed and not settings.DEBUG: |         if not authed and not settings.DEBUG: | ||||||
|             return HttpResponse(status=401) |             response = HttpResponse(status=401) | ||||||
|  |             response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"' | ||||||
|  |             return response | ||||||
|  |  | ||||||
|         monitoring_set.send_robust(self) |         monitoring_set.send_robust(self) | ||||||
|  |  | ||||||
|         return ExportToDjangoView(request) |         return ExportToDjangoView(request) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,9 +1,8 @@ | |||||||
| """root tests""" | """root tests""" | ||||||
|  |  | ||||||
| from pathlib import Path | from base64 import b64encode | ||||||
| from secrets import token_urlsafe |  | ||||||
| from tempfile import gettempdir |  | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| @ -11,16 +10,6 @@ from django.urls import reverse | |||||||
| class TestRoot(TestCase): | class TestRoot(TestCase): | ||||||
|     """Test root application""" |     """Test root application""" | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         _tmp = Path(gettempdir()) |  | ||||||
|         self.token = token_urlsafe(32) |  | ||||||
|         with open(_tmp / "authentik-core-metrics.key", "w") as _f: |  | ||||||
|             _f.write(self.token) |  | ||||||
|  |  | ||||||
|     def tearDown(self): |  | ||||||
|         _tmp = Path(gettempdir()) |  | ||||||
|         (_tmp / "authentik-core-metrics.key").unlink() |  | ||||||
|  |  | ||||||
|     def test_monitoring_error(self): |     def test_monitoring_error(self): | ||||||
|         """Test monitoring without any credentials""" |         """Test monitoring without any credentials""" | ||||||
|         response = self.client.get(reverse("metrics")) |         response = self.client.get(reverse("metrics")) | ||||||
| @ -28,7 +17,8 @@ class TestRoot(TestCase): | |||||||
|  |  | ||||||
|     def test_monitoring_ok(self): |     def test_monitoring_ok(self): | ||||||
|         """Test monitoring with credentials""" |         """Test monitoring with credentials""" | ||||||
|         auth_headers = {"HTTP_AUTHORIZATION": f"Bearer {self.token}"} |         creds = "Basic " + b64encode(f"monitor:{settings.SECRET_KEY}".encode()).decode("utf-8") | ||||||
|  |         auth_headers = {"HTTP_AUTHORIZATION": creds} | ||||||
|         response = self.client.get(reverse("metrics"), **auth_headers) |         response = self.client.get(reverse("metrics"), **auth_headers) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -2,7 +2,7 @@ | |||||||
|     "$schema": "http://json-schema.org/draft-07/schema", |     "$schema": "http://json-schema.org/draft-07/schema", | ||||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", |     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||||
|     "type": "object", |     "type": "object", | ||||||
|     "title": "authentik 2024.8.6 Blueprint schema", |     "title": "authentik 2024.8.3 Blueprint schema", | ||||||
|     "required": [ |     "required": [ | ||||||
|         "version", |         "version", | ||||||
|         "entries" |         "entries" | ||||||
| @ -5345,30 +5345,9 @@ | |||||||
|                     "description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256." |                     "description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256." | ||||||
|                 }, |                 }, | ||||||
|                 "redirect_uris": { |                 "redirect_uris": { | ||||||
|                     "type": "array", |  | ||||||
|                     "items": { |  | ||||||
|                         "type": "object", |  | ||||||
|                         "properties": { |  | ||||||
|                             "matching_mode": { |  | ||||||
|                     "type": "string", |                     "type": "string", | ||||||
|                                 "enum": [ |                     "title": "Redirect URIs", | ||||||
|                                     "strict", |                     "description": "Enter each URI on a new line." | ||||||
|                                     "regex" |  | ||||||
|                                 ], |  | ||||||
|                                 "title": "Matching mode" |  | ||||||
|                             }, |  | ||||||
|                             "url": { |  | ||||||
|                                 "type": "string", |  | ||||||
|                                 "minLength": 1, |  | ||||||
|                                 "title": "Url" |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         "required": [ |  | ||||||
|                             "matching_mode", |  | ||||||
|                             "url" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     "title": "Redirect uris" |  | ||||||
|                 }, |                 }, | ||||||
|                 "sub_mode": { |                 "sub_mode": { | ||||||
|                     "type": "string", |                     "type": "string", | ||||||
|  | |||||||
| @ -14,7 +14,11 @@ entries: | |||||||
|       expression: | |       expression: | | ||||||
|         # This mapping is used by the authentik proxy. It passes extra user attributes, |         # This mapping is used by the authentik proxy. It passes extra user attributes, | ||||||
|         # which are used for example for the HTTP-Basic Authentication mapping. |         # which are used for example for the HTTP-Basic Authentication mapping. | ||||||
|  |         session_id = None | ||||||
|  |         if "token" in request.context: | ||||||
|  |             session_id = request.context.get("token").session_id | ||||||
|         return { |         return { | ||||||
|  |             "sid": session_id, | ||||||
|             "ak_proxy": { |             "ak_proxy": { | ||||||
|                 "user_attributes": request.user.group_attributes(request), |                 "user_attributes": request.user.group_attributes(request), | ||||||
|                 "is_superuser": request.user.is_superuser, |                 "is_superuser": request.user.is_superuser, | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.6} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.3} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -52,7 +52,7 @@ services: | |||||||
|       - postgresql |       - postgresql | ||||||
|       - redis |       - redis | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.6} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.3} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								go.mod
									
									
									
									
									
								
							| @ -7,7 +7,7 @@ toolchain go1.23.0 | |||||||
| require ( | require ( | ||||||
| 	beryju.io/ldap v0.1.0 | 	beryju.io/ldap v0.1.0 | ||||||
| 	github.com/coreos/go-oidc/v3 v3.11.0 | 	github.com/coreos/go-oidc/v3 v3.11.0 | ||||||
| 	github.com/getsentry/sentry-go v0.28.1 | 	github.com/getsentry/sentry-go v0.29.0 | ||||||
| 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.8 | 	github.com/go-ldap/ldap/v3 v3.4.8 | ||||||
| 	github.com/go-openapi/runtime v0.28.0 | 	github.com/go-openapi/runtime v0.28.0 | ||||||
| @ -18,20 +18,20 @@ require ( | |||||||
| 	github.com/gorilla/securecookie v1.1.2 | 	github.com/gorilla/securecookie v1.1.2 | ||||||
| 	github.com/gorilla/sessions v1.4.0 | 	github.com/gorilla/sessions v1.4.0 | ||||||
| 	github.com/gorilla/websocket v1.5.3 | 	github.com/gorilla/websocket v1.5.3 | ||||||
| 	github.com/jellydator/ttlcache/v3 v3.2.1 | 	github.com/jellydator/ttlcache/v3 v3.3.0 | ||||||
| 	github.com/mitchellh/mapstructure v1.5.0 | 	github.com/mitchellh/mapstructure v1.5.0 | ||||||
| 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||||
| 	github.com/pires/go-proxyproto v0.7.0 | 	github.com/pires/go-proxyproto v0.7.0 | ||||||
| 	github.com/prometheus/client_golang v1.20.2 | 	github.com/prometheus/client_golang v1.20.4 | ||||||
| 	github.com/redis/go-redis/v9 v9.6.1 | 	github.com/redis/go-redis/v9 v9.6.1 | ||||||
| 	github.com/sethvargo/go-envconfig v1.1.0 | 	github.com/sethvargo/go-envconfig v1.1.0 | ||||||
| 	github.com/sirupsen/logrus v1.9.3 | 	github.com/sirupsen/logrus v1.9.3 | ||||||
| 	github.com/spf13/cobra v1.8.1 | 	github.com/spf13/cobra v1.8.1 | ||||||
| 	github.com/stretchr/testify v1.9.0 | 	github.com/stretchr/testify v1.9.0 | ||||||
| 	github.com/wwt/guac v1.3.2 | 	github.com/wwt/guac v1.3.2 | ||||||
| 	goauthentik.io/api/v3 v3.2024064.1 | 	goauthentik.io/api/v3 v3.2024083.1 | ||||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||||
| 	golang.org/x/oauth2 v0.22.0 | 	golang.org/x/oauth2 v0.23.0 | ||||||
| 	golang.org/x/sync v0.8.0 | 	golang.org/x/sync v0.8.0 | ||||||
| 	gopkg.in/yaml.v2 v2.4.0 | 	gopkg.in/yaml.v2 v2.4.0 | ||||||
| 	layeh.com/radius v0.0.0-20210819152912-ad72663a72ab | 	layeh.com/radius v0.0.0-20210819152912-ad72663a72ab | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.sum
									
									
									
									
									
								
							| @ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m | |||||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||||
| github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | ||||||
| github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||||||
| github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= | github.com/getsentry/sentry-go v0.29.0 h1:YtWluuCFg9OfcqnaujpY918N/AhCCwarIDWOYSBAjCA= | ||||||
| github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= | github.com/getsentry/sentry-go v0.29.0/go.mod h1:jhPesDAL0Q0W2+2YEuVOvdWmVtdsr1+jtBrlDEVWwLY= | ||||||
| github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= | github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= | ||||||
| github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||||
| github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||||||
| @ -200,8 +200,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 | |||||||
| github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= | ||||||
| github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= | ||||||
| github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= | ||||||
| github.com/jellydator/ttlcache/v3 v3.2.1 h1:eS8ljnYY7BllYGkXw/TfczWZrXUu/CH7SIkC6ugn9Js= | github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= | ||||||
| github.com/jellydator/ttlcache/v3 v3.2.1/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= | github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= | ||||||
| github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= | ||||||
| github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | ||||||
| github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||||
| @ -239,8 +239,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | |||||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= | github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= | ||||||
| github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= | github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= | ||||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | ||||||
| github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | ||||||
| @ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | |||||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||||
| goauthentik.io/api/v3 v3.2024064.1 h1:vxquklgDGD+nGFhWRAsQ7ezQKg17MRq6bzEk25fbsb4= | goauthentik.io/api/v3 v3.2024083.1 h1:OPo2VejMkS5WjYw5zIjfuxR9XUbTKs4m+sACrPKcm9U= | ||||||
| goauthentik.io/api/v3 v3.2024064.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | goauthentik.io/api/v3 v3.2024083.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| @ -388,8 +388,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr | |||||||
| golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | ||||||
| golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= | ||||||
| golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2024.8.6" | const VERSION = "2024.8.3" | ||||||
|  | |||||||
| @ -65,8 +65,11 @@ type Server interface { | |||||||
| 	CryptoStore() *ak.CryptoStore | 	CryptoStore() *ak.CryptoStore | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*Application, error) { | func init() { | ||||||
| 	gob.Register(Claims{}) | 	gob.Register(Claims{}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, oldApp *Application) (*Application, error) { | ||||||
| 	muxLogger := log.WithField("logger", "authentik.outpost.proxyv2.application").WithField("name", p.Name) | 	muxLogger := log.WithField("logger", "authentik.outpost.proxyv2.application").WithField("name", p.Name) | ||||||
|  |  | ||||||
| 	externalHost, err := url.Parse(p.ExternalHost) | 	externalHost, err := url.Parse(p.ExternalHost) | ||||||
| @ -137,7 +140,15 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A | |||||||
| 		isEmbedded:           isEmbedded, | 		isEmbedded:           isEmbedded, | ||||||
| 	} | 	} | ||||||
| 	go a.authHeaderCache.Start() | 	go a.authHeaderCache.Start() | ||||||
| 	a.sessions = a.getStore(p, externalHost) | 	if oldApp != nil && oldApp.sessions != nil { | ||||||
|  | 		a.sessions = oldApp.sessions | ||||||
|  | 	} else { | ||||||
|  | 		sess, err := a.getStore(p, externalHost) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		a.sessions = sess | ||||||
|  | 	} | ||||||
| 	mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry { | 	mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry { | ||||||
| 		c := a.getClaimsFromSession(r) | 		c := a.getClaimsFromSession(r) | ||||||
| 		if c == nil { | 		if c == nil { | ||||||
| @ -235,9 +246,8 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A | |||||||
| 				// TODO: maybe create event for this? | 				// TODO: maybe create event for this? | ||||||
| 				a.log.WithError(err).Warning("failed to compile SkipPathRegex") | 				a.log.WithError(err).Warning("failed to compile SkipPathRegex") | ||||||
| 				continue | 				continue | ||||||
| 			} else { |  | ||||||
| 				a.UnauthenticatedRegex = append(a.UnauthenticatedRegex, re) |  | ||||||
| 			} | 			} | ||||||
|  | 			a.UnauthenticatedRegex = append(a.UnauthenticatedRegex, re) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return a, nil | 	return a, nil | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ import ( | |||||||
|  |  | ||||||
| const RedisKeyPrefix = "authentik_proxy_session_" | const RedisKeyPrefix = "authentik_proxy_session_" | ||||||
|  |  | ||||||
| func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store { | func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) (sessions.Store, error) { | ||||||
| 	maxAge := 0 | 	maxAge := 0 | ||||||
| 	if p.AccessTokenValidity.IsSet() { | 	if p.AccessTokenValidity.IsSet() { | ||||||
| 		t := p.AccessTokenValidity.Get() | 		t := p.AccessTokenValidity.Get() | ||||||
| @ -73,7 +73,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) | |||||||
| 		// New default RedisStore | 		// New default RedisStore | ||||||
| 		rs, err := redisstore.NewRedisStore(context.Background(), client) | 		rs, err := redisstore.NewRedisStore(context.Background(), client) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			a.log.WithError(err).Panic("failed to connect to redis") | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		rs.KeyPrefix(RedisKeyPrefix) | 		rs.KeyPrefix(RedisKeyPrefix) | ||||||
| @ -87,7 +87,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) | |||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		a.log.Trace("using redis session backend") | 		a.log.Trace("using redis session backend") | ||||||
| 		return rs | 		return rs, nil | ||||||
| 	} | 	} | ||||||
| 	dir := os.TempDir() | 	dir := os.TempDir() | ||||||
| 	cs := sessions.NewFilesystemStore(dir) | 	cs := sessions.NewFilesystemStore(dir) | ||||||
| @ -106,7 +106,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) | |||||||
| 	cs.Options.MaxAge = maxAge | 	cs.Options.MaxAge = maxAge | ||||||
| 	cs.Options.Path = "/" | 	cs.Options.Path = "/" | ||||||
| 	a.log.WithField("dir", dir).Trace("using filesystem session backend") | 	a.log.WithField("dir", dir).Trace("using filesystem session backend") | ||||||
| 	return cs | 	return cs, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) SessionName() string { | func (a *Application) SessionName() string { | ||||||
|  | |||||||
| @ -66,6 +66,7 @@ func newTestApplication() *Application { | |||||||
| 		}, | 		}, | ||||||
| 		http.DefaultClient, | 		http.DefaultClient, | ||||||
| 		ts, | 		ts, | ||||||
|  | 		nil, | ||||||
| 	) | 	) | ||||||
| 	ts.apps = append(ts.apps, a) | 	ts.apps = append(ts.apps, a) | ||||||
| 	return a | 	return a | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" |  | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	sentryhttp "github.com/getsentry/sentry-go/http" | 	sentryhttp "github.com/getsentry/sentry-go/http" | ||||||
| @ -71,20 +70,12 @@ func NewProxyServer(ac *ak.APIController) *ProxyServer { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (ps *ProxyServer) HandleHost(rw http.ResponseWriter, r *http.Request) bool { | func (ps *ProxyServer) HandleHost(rw http.ResponseWriter, r *http.Request) bool { | ||||||
| 	// Always handle requests for outpost paths that should answer regardless of hostname |  | ||||||
| 	if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io/ping") || |  | ||||||
| 		strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io/static") { |  | ||||||
| 		ps.mux.ServeHTTP(rw, r) |  | ||||||
| 		return true |  | ||||||
| 	} |  | ||||||
| 	// lookup app by hostname |  | ||||||
| 	a, _ := ps.lookupApp(r) | 	a, _ := ps.lookupApp(r) | ||||||
| 	if a == nil { | 	if a == nil { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	// check if the app should handle this URL, or is setup in proxy mode |  | ||||||
| 	if a.ShouldHandleURL(r) || a.Mode() == api.PROXYMODE_PROXY { | 	if a.ShouldHandleURL(r) || a.Mode() == api.PROXYMODE_PROXY { | ||||||
| 		ps.mux.ServeHTTP(rw, r) | 		a.ServeHTTP(rw, r) | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  |  | ||||||
| 	"github.com/getsentry/sentry-go" | 	"github.com/getsentry/sentry-go" | ||||||
| 	"goauthentik.io/internal/constants" | 	"goauthentik.io/internal/constants" | ||||||
| @ -37,16 +38,21 @@ func (ps *ProxyServer) Refresh() error { | |||||||
| 				), | 				), | ||||||
| 			), | 			), | ||||||
| 		} | 		} | ||||||
| 		a, err := application.NewApplication(provider, hc, ps) | 		externalHost, err := url.Parse(provider.ExternalHost) | ||||||
| 		existing, ok := ps.apps[a.Host] | 		if err != nil { | ||||||
|  | 			ps.log.WithError(err).Warning("failed to parse URL, skipping provider") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		existing, ok := ps.apps[externalHost.Host] | ||||||
|  | 		a, err := application.NewApplication(provider, hc, ps, existing) | ||||||
| 		if ok { | 		if ok { | ||||||
| 			existing.Stop() | 			existing.Stop() | ||||||
| 		} | 		} | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ps.log.WithError(err).Warning("failed to setup application") | 			ps.log.WithError(err).Warning("failed to setup application") | ||||||
| 		} else { | 			continue | ||||||
| 			apps[a.Host] = a |  | ||||||
| 		} | 		} | ||||||
|  | 		apps[externalHost.Host] = a | ||||||
| 	} | 	} | ||||||
| 	ps.apps = apps | 	ps.apps = apps | ||||||
| 	ps.log.Debug("Swapped maps") | 	ps.log.Debug("Swapped maps") | ||||||
|  | |||||||
| @ -1,15 +1,11 @@ | |||||||
| package web | package web | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/base64" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
|  |  | ||||||
| 	"github.com/gorilla/mux" | 	"github.com/gorilla/mux" | ||||||
| 	"github.com/gorilla/securecookie" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus" | 	"github.com/prometheus/client_golang/prometheus" | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" | 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promhttp" | 	"github.com/prometheus/client_golang/prometheus/promhttp" | ||||||
| @ -18,25 +14,14 @@ import ( | |||||||
| 	"goauthentik.io/internal/utils/sentry" | 	"goauthentik.io/internal/utils/sentry" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const MetricsKeyFile = "authentik-core-metrics.key" |  | ||||||
|  |  | ||||||
| var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{ | var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{ | ||||||
| 	Name: "authentik_main_request_duration_seconds", | 	Name: "authentik_main_request_duration_seconds", | ||||||
| 	Help: "API request latencies in seconds", | 	Help: "API request latencies in seconds", | ||||||
| }, []string{"dest"}) | }, []string{"dest"}) | ||||||
|  |  | ||||||
| func (ws *WebServer) runMetricsServer() { | func (ws *WebServer) runMetricsServer() { | ||||||
| 	l := log.WithField("logger", "authentik.router.metrics") |  | ||||||
| 	tmp := os.TempDir() |  | ||||||
| 	key := base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(64)) |  | ||||||
| 	keyPath := path.Join(tmp, MetricsKeyFile) |  | ||||||
| 	err := os.WriteFile(keyPath, []byte(key), 0o600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		l.WithError(err).Warning("failed to save metrics key") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	m := mux.NewRouter() | 	m := mux.NewRouter() | ||||||
|  | 	l := log.WithField("logger", "authentik.router.metrics") | ||||||
| 	m.Use(sentry.SentryNoSampleMiddleware) | 	m.Use(sentry.SentryNoSampleMiddleware) | ||||||
| 	m.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | 	m.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 		promhttp.InstrumentMetricHandler( | 		promhttp.InstrumentMetricHandler( | ||||||
| @ -51,7 +36,7 @@ func (ws *WebServer) runMetricsServer() { | |||||||
| 			l.WithError(err).Warning("failed to get upstream metrics") | 			l.WithError(err).Warning("failed to get upstream metrics") | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		re.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key)) | 		re.SetBasicAuth("monitor", config.Get().SecretKey) | ||||||
| 		res, err := ws.upstreamHttpClient().Do(re) | 		res, err := ws.upstreamHttpClient().Do(re) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			l.WithError(err).Warning("failed to get upstream metrics") | 			l.WithError(err).Warning("failed to get upstream metrics") | ||||||
| @ -64,13 +49,9 @@ func (ws *WebServer) runMetricsServer() { | |||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| 	l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server") | 	l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server") | ||||||
| 	err = http.ListenAndServe(config.Get().Listen.Metrics, m) | 	err := http.ListenAndServe(config.Get().Listen.Metrics, m) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.WithError(err).Warning("Failed to start metrics server") | 		l.WithError(err).Warning("Failed to start metrics server") | ||||||
| 	} | 	} | ||||||
| 	l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server") | 	l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server") | ||||||
| 	err = os.Remove(keyPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		l.WithError(err).Warning("failed to remove metrics key file") |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -52,7 +52,7 @@ func NewWebServer() *WebServer { | |||||||
| 	loggingHandler.Use(web.NewLoggingHandler(l, nil)) | 	loggingHandler.Use(web.NewLoggingHandler(l, nil)) | ||||||
|  |  | ||||||
| 	tmp := os.TempDir() | 	tmp := os.TempDir() | ||||||
| 	socketPath := path.Join(tmp, UnixSocketName) | 	socketPath := path.Join(tmp, "authentik-core.sock") | ||||||
|  |  | ||||||
| 	// create http client to talk to backend, normal client if we're in debug more | 	// create http client to talk to backend, normal client if we're in debug more | ||||||
| 	// and a client that connects to our socket when in non debug mode | 	// and a client that connects to our socket when in non debug mode | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-08-18 00:08+0000\n" | "POT-Creation-Date: 2024-09-25 00:08+0000\n" | ||||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | "Language-Team: LANGUAGE <LL@li.org>\n" | ||||||
| @ -18,6 +18,11 @@ msgstr "" | |||||||
| "Content-Transfer-Encoding: 8bit\n" | "Content-Transfer-Encoding: 8bit\n" | ||||||
| "Plural-Forms: nplurals=2; plural=(n != 1);\n" | "Plural-Forms: nplurals=2; plural=(n != 1);\n" | ||||||
|  |  | ||||||
|  | #: authentik/admin/tasks.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "New version {version} available!" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/api/schema.py | #: authentik/api/schema.py | ||||||
| msgid "Generic API Error" | msgid "Generic API Error" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1844,6 +1849,10 @@ msgstr "" | |||||||
| msgid "Used recovery-link to authenticate." | msgid "Used recovery-link to authenticate." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/sources/ldap/api.py | ||||||
|  | msgid "Only a single LDAP Source with password synchronization is allowed" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Server URI" | msgid "Server URI" | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -19,7 +19,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-08-12 13:45+0000\n" | "POT-Creation-Date: 2024-09-08 00:09+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Marc Schmitt, 2024\n" | "Last-Translator: Marc Schmitt, 2024\n" | ||||||
| "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" | "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" | ||||||
| @ -29,6 +29,11 @@ msgstr "" | |||||||
| "Language: fr\n" | "Language: fr\n" | ||||||
| "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" | ||||||
|  |  | ||||||
|  | #: authentik/admin/tasks.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "New version {version} available!" | ||||||
|  | msgstr "Une nouvelle version {version} est disponible !" | ||||||
|  |  | ||||||
| #: authentik/api/schema.py | #: authentik/api/schema.py | ||||||
| msgid "Generic API Error" | msgid "Generic API Error" | ||||||
| msgstr "Erreur d'API Générique" | msgstr "Erreur d'API Générique" | ||||||
| @ -1342,14 +1347,6 @@ msgstr "Impossible de résoudre l'application" | |||||||
| msgid "DN under which objects are accessible." | msgid "DN under which objects are accessible." | ||||||
| msgstr "DN sous lequel les objets sont accessibles." | msgstr "DN sous lequel les objets sont accessibles." | ||||||
|  |  | ||||||
| #: authentik/providers/ldap/models.py |  | ||||||
| msgid "" |  | ||||||
| "Users in this group can do search queries. If not set, every user can " |  | ||||||
| "execute search queries." |  | ||||||
| msgstr "" |  | ||||||
| "Les utilisateurs dans ce groupe peuvent faire des requêtes de recherche. Si " |  | ||||||
| "laissé vide, tous les utilisateurs peuvent faire des requêtes de recherche." |  | ||||||
|  |  | ||||||
| #: authentik/providers/ldap/models.py | #: authentik/providers/ldap/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "The start for uidNumbers, this number is added to the user.pk to make sure " | "The start for uidNumbers, this number is added to the user.pk to make sure " | ||||||
| @ -1396,6 +1393,10 @@ msgstr "Fournisseur LDAP" | |||||||
| msgid "LDAP Providers" | msgid "LDAP Providers" | ||||||
| msgstr "Fournisseurs LDAP" | msgstr "Fournisseurs LDAP" | ||||||
|  |  | ||||||
|  | #: authentik/providers/ldap/models.py | ||||||
|  | msgid "Search full LDAP directory" | ||||||
|  | msgstr "Rechercher dans l'annuaire LDAP complet" | ||||||
|  |  | ||||||
| #: authentik/providers/oauth2/id_token.py | #: authentik/providers/oauth2/id_token.py | ||||||
| msgid "Based on the Hashed User ID" | msgid "Based on the Hashed User ID" | ||||||
| msgstr "Basé sur le hash de l'ID utilisateur" | msgstr "Basé sur le hash de l'ID utilisateur" | ||||||
| @ -1796,6 +1797,14 @@ msgstr "Mappage de propriété fournisseur Radius" | |||||||
| msgid "Radius Provider Property Mappings" | msgid "Radius Provider Property Mappings" | ||||||
| msgstr "Mappages de propriété fournisseur Radius" | msgstr "Mappages de propriété fournisseur Radius" | ||||||
|  |  | ||||||
|  | #: authentik/providers/saml/api/providers.py | ||||||
|  | msgid "" | ||||||
|  | "With a signing keypair selected, at least one of 'Sign assertion' and 'Sign " | ||||||
|  | "Response' must be selected." | ||||||
|  | msgstr "" | ||||||
|  | "Quand une clé de signature est sélectionnée, au moins l'un de « Signer les " | ||||||
|  | "assertions » et « Signer les réponses » doit être sélectionné." | ||||||
|  |  | ||||||
| #: authentik/providers/saml/api/providers.py | #: authentik/providers/saml/api/providers.py | ||||||
| msgid "Invalid XML Syntax" | msgid "Invalid XML Syntax" | ||||||
| msgstr "Syntaxe XML Invalide" | msgstr "Syntaxe XML Invalide" | ||||||
| @ -1944,6 +1953,20 @@ msgstr "" | |||||||
| msgid "Signing Keypair" | msgid "Signing Keypair" | ||||||
| msgstr "Paire de clés de Signature" | msgstr "Paire de clés de Signature" | ||||||
|  |  | ||||||
|  | #: authentik/providers/saml/models.py authentik/sources/saml/models.py | ||||||
|  | msgid "" | ||||||
|  | "When selected, incoming assertions are encrypted by the IdP using the public" | ||||||
|  | " key of the encryption keypair. The assertion is decrypted by the SP using " | ||||||
|  | "the the private key." | ||||||
|  | msgstr "" | ||||||
|  | "Si activé, les assertions entrantes seront chiffrées par l'IdP avec la clé " | ||||||
|  | "publique de la paire de clé de chiffrement. L'assertion est déchiffrée par " | ||||||
|  | "le SP en utilisant la clé privée." | ||||||
|  |  | ||||||
|  | #: authentik/providers/saml/models.py authentik/sources/saml/models.py | ||||||
|  | msgid "Encryption Keypair" | ||||||
|  | msgstr "Paire de clés de chiffrement" | ||||||
|  |  | ||||||
| #: authentik/providers/saml/models.py | #: authentik/providers/saml/models.py | ||||||
| msgid "Default relay_state value for IDP-initiated logins" | msgid "Default relay_state value for IDP-initiated logins" | ||||||
| msgstr "Valeur par défaut de relay_state des connexions initiées par l'IdP" | msgstr "Valeur par défaut de relay_state des connexions initiées par l'IdP" | ||||||
| @ -2466,20 +2489,6 @@ msgstr "" | |||||||
| "Paire de clés utilisées pour signer les réponses sortantes allant vers le " | "Paire de clés utilisées pour signer les réponses sortantes allant vers le " | ||||||
| "fournisseur d'identité." | "fournisseur d'identité." | ||||||
|  |  | ||||||
| #: authentik/sources/saml/models.py |  | ||||||
| msgid "" |  | ||||||
| "When selected, incoming assertions are encrypted by the IdP using the public" |  | ||||||
| " key of the encryption keypair. The assertion is decrypted by the SP using " |  | ||||||
| "the the private key." |  | ||||||
| msgstr "" |  | ||||||
| "Si activé, les assertions entrantes seront chiffrées par l'IdP avec la clé " |  | ||||||
| "publique de la paire de clé de chiffrement. L'assertion est déchiffrée par " |  | ||||||
| "le SP en utilisant la clé privée." |  | ||||||
|  |  | ||||||
| #: authentik/sources/saml/models.py |  | ||||||
| msgid "Encryption Keypair" |  | ||||||
| msgstr "Paire de clés de chiffrement" |  | ||||||
|  |  | ||||||
| #: authentik/sources/saml/models.py | #: authentik/sources/saml/models.py | ||||||
| msgid "SAML Source" | msgid "SAML Source" | ||||||
| msgstr "Source SAML" | msgstr "Source SAML" | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-08-15 00:09+0000\n" | "POT-Creation-Date: 2024-08-18 00:08+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: Anton Babenko, 2024\n" | "Last-Translator: Anton Babenko, 2024\n" | ||||||
| "Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n" | "Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n" | ||||||
| @ -739,7 +739,7 @@ msgstr "Правило Уведомления" | |||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "Notification Rules" | msgid "Notification Rules" | ||||||
| msgstr "Правило Уведомлений" | msgstr "Правила уведомлений" | ||||||
|  |  | ||||||
| #: authentik/events/models.py | #: authentik/events/models.py | ||||||
| msgid "Webhook Mapping" | msgid "Webhook Mapping" | ||||||
| @ -1771,6 +1771,14 @@ msgstr "Сопоставление свойства Radius провайдера" | |||||||
| msgid "Radius Provider Property Mappings" | msgid "Radius Provider Property Mappings" | ||||||
| msgstr "Сопоставление свойств Radius провайдера" | msgstr "Сопоставление свойств Radius провайдера" | ||||||
|  |  | ||||||
|  | #: authentik/providers/saml/api/providers.py | ||||||
|  | msgid "" | ||||||
|  | "With a signing keypair selected, at least one of 'Sign assertion' and 'Sign " | ||||||
|  | "Response' must be selected." | ||||||
|  | msgstr "" | ||||||
|  | "При выборе пары ключей для подписи необходимо выбрать как минимум один из " | ||||||
|  | "вариантов: 'Подписывать утверждение' или 'Подписывать ответ'." | ||||||
|  |  | ||||||
| #: authentik/providers/saml/api/providers.py | #: authentik/providers/saml/api/providers.py | ||||||
| msgid "Invalid XML Syntax" | msgid "Invalid XML Syntax" | ||||||
| msgstr "Некорректный синтаксис XML" | msgstr "Некорректный синтаксис XML" | ||||||
| @ -1918,6 +1926,21 @@ msgstr "" | |||||||
| msgid "Signing Keypair" | msgid "Signing Keypair" | ||||||
| msgstr "Пара ключей для подписи" | msgstr "Пара ключей для подписи" | ||||||
|  |  | ||||||
|  | #: authentik/providers/saml/models.py authentik/sources/saml/models.py | ||||||
|  | msgid "" | ||||||
|  | "When selected, incoming assertions are encrypted by the IdP using the public" | ||||||
|  | " key of the encryption keypair. The assertion is decrypted by the SP using " | ||||||
|  | "the the private key." | ||||||
|  | msgstr "" | ||||||
|  | "При выборе этого варианта, входящие утверждения шифруются поставщиком " | ||||||
|  | "идентификации (IdP) с использованием открытого ключа из пары ключей " | ||||||
|  | "шифрования. Утверждение расшифровывается поставщиком услуг (SP) с " | ||||||
|  | "использованием закрытого ключа." | ||||||
|  |  | ||||||
|  | #: authentik/providers/saml/models.py authentik/sources/saml/models.py | ||||||
|  | msgid "Encryption Keypair" | ||||||
|  | msgstr "Пара ключей шифрования" | ||||||
|  |  | ||||||
| #: authentik/providers/saml/models.py | #: authentik/providers/saml/models.py | ||||||
| msgid "Default relay_state value for IDP-initiated logins" | msgid "Default relay_state value for IDP-initiated logins" | ||||||
| msgstr "Значение relay_state по умолчанию для логинов, инициированных IDP" | msgstr "Значение relay_state по умолчанию для логинов, инициированных IDP" | ||||||
| @ -2446,21 +2469,6 @@ msgstr "" | |||||||
| "Пара ключей, используемая для подписи исходящих ответов, направляемых " | "Пара ключей, используемая для подписи исходящих ответов, направляемых " | ||||||
| "провайдеру идентификационных данных." | "провайдеру идентификационных данных." | ||||||
|  |  | ||||||
| #: authentik/sources/saml/models.py |  | ||||||
| msgid "" |  | ||||||
| "When selected, incoming assertions are encrypted by the IdP using the public" |  | ||||||
| " key of the encryption keypair. The assertion is decrypted by the SP using " |  | ||||||
| "the the private key." |  | ||||||
| msgstr "" |  | ||||||
| "При выборе этого варианта, входящие утверждения шифруются поставщиком " |  | ||||||
| "идентификации (IdP) с использованием открытого ключа из пары ключей " |  | ||||||
| "шифрования. Утверждение расшифровывается поставщиком услуг (SP) с " |  | ||||||
| "использованием закрытого ключа." |  | ||||||
|  |  | ||||||
| #: authentik/sources/saml/models.py |  | ||||||
| msgid "Encryption Keypair" |  | ||||||
| msgstr "Пара ключей шифрования" |  | ||||||
|  |  | ||||||
| #: authentik/sources/saml/models.py | #: authentik/sources/saml/models.py | ||||||
| msgid "SAML Source" | msgid "SAML Source" | ||||||
| msgstr "Источник SAML" | msgstr "Источник SAML" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-08-18 00:08+0000\n" | "POT-Creation-Date: 2024-09-25 00:08+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2024\n" | "Last-Translator: deluxghost, 2024\n" | ||||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | ||||||
| @ -25,6 +25,11 @@ msgstr "" | |||||||
| "Language: zh-Hans\n" | "Language: zh-Hans\n" | ||||||
| "Plural-Forms: nplurals=1; plural=0;\n" | "Plural-Forms: nplurals=1; plural=0;\n" | ||||||
|  |  | ||||||
|  | #: authentik/admin/tasks.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "New version {version} available!" | ||||||
|  | msgstr "新版本 {version} 可用!" | ||||||
|  |  | ||||||
| #: authentik/api/schema.py | #: authentik/api/schema.py | ||||||
| msgid "Generic API Error" | msgid "Generic API Error" | ||||||
| msgstr "通用 API 错误" | msgstr "通用 API 错误" | ||||||
| @ -1877,6 +1882,10 @@ msgstr "创建一个密钥,可用于恢复对 authentik 的访问权限。" | |||||||
| msgid "Used recovery-link to authenticate." | msgid "Used recovery-link to authenticate." | ||||||
| msgstr "已使用恢复链接进行身份验证。" | msgstr "已使用恢复链接进行身份验证。" | ||||||
|  |  | ||||||
|  | #: authentik/sources/ldap/api.py | ||||||
|  | msgid "Only a single LDAP Source with password synchronization is allowed" | ||||||
|  | msgstr "仅允许使用密码同步的单个 LDAP 源" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Server URI" | msgid "Server URI" | ||||||
| msgstr "服务器 URI" | msgstr "服务器 URI" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -14,7 +14,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-08-18 00:08+0000\n" | "POT-Creation-Date: 2024-09-25 00:08+0000\n" | ||||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||||
| "Last-Translator: deluxghost, 2024\n" | "Last-Translator: deluxghost, 2024\n" | ||||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | ||||||
| @ -24,6 +24,11 @@ msgstr "" | |||||||
| "Language: zh_CN\n" | "Language: zh_CN\n" | ||||||
| "Plural-Forms: nplurals=1; plural=0;\n" | "Plural-Forms: nplurals=1; plural=0;\n" | ||||||
|  |  | ||||||
|  | #: authentik/admin/tasks.py | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "New version {version} available!" | ||||||
|  | msgstr "新版本 {version} 可用!" | ||||||
|  |  | ||||||
| #: authentik/api/schema.py | #: authentik/api/schema.py | ||||||
| msgid "Generic API Error" | msgid "Generic API Error" | ||||||
| msgstr "通用 API 错误" | msgstr "通用 API 错误" | ||||||
| @ -1876,6 +1881,10 @@ msgstr "创建一个密钥,可用于恢复对 authentik 的访问权限。" | |||||||
| msgid "Used recovery-link to authenticate." | msgid "Used recovery-link to authenticate." | ||||||
| msgstr "已使用恢复链接进行身份验证。" | msgstr "已使用恢复链接进行身份验证。" | ||||||
|  |  | ||||||
|  | #: authentik/sources/ldap/api.py | ||||||
|  | msgid "Only a single LDAP Source with password synchronization is allowed" | ||||||
|  | msgstr "仅允许使用密码同步的单个 LDAP 源" | ||||||
|  |  | ||||||
| #: authentik/sources/ldap/models.py | #: authentik/sources/ldap/models.py | ||||||
| msgid "Server URI" | msgid "Server URI" | ||||||
| msgstr "服务器 URI" | msgstr "服务器 URI" | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|     "name": "@goauthentik/authentik", |     "name": "@goauthentik/authentik", | ||||||
|     "version": "2024.8.6", |     "version": "2024.8.3", | ||||||
|     "private": true |     "private": true | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										569
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										569
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -366,13 +366,13 @@ typing-extensions = ">=4.0.0" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "bandit" | name = "bandit" | ||||||
| version = "1.7.9" | version = "1.7.10" | ||||||
| description = "Security oriented static analyser for python code." | description = "Security oriented static analyser for python code." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, |     {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, | ||||||
|     {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, |     {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -1134,46 +1134,49 @@ tests = ["django", "hypothesis", "pytest", "pytest-asyncio"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "debugpy" | name = "debugpy" | ||||||
| version = "1.8.5" | version = "1.8.6" | ||||||
| description = "An implementation of the Debug Adapter Protocol for Python" | description = "An implementation of the Debug Adapter Protocol for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, |     {file = "debugpy-1.8.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:30f467c5345d9dfdcc0afdb10e018e47f092e383447500f125b4e013236bf14b"}, | ||||||
|     {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, |     {file = "debugpy-1.8.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d73d8c52614432f4215d0fe79a7e595d0dd162b5c15233762565be2f014803b"}, | ||||||
|     {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, |     {file = "debugpy-1.8.6-cp310-cp310-win32.whl", hash = "sha256:e3e182cd98eac20ee23a00653503315085b29ab44ed66269482349d307b08df9"}, | ||||||
|     {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, |     {file = "debugpy-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:e3a82da039cfe717b6fb1886cbbe5c4a3f15d7df4765af857f4307585121c2dd"}, | ||||||
|     {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, |     {file = "debugpy-1.8.6-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67479a94cf5fd2c2d88f9615e087fcb4fec169ec780464a3f2ba4a9a2bb79955"}, | ||||||
|     {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, |     {file = "debugpy-1.8.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb8653f6cbf1dd0a305ac1aa66ec246002145074ea57933978346ea5afdf70b"}, | ||||||
|     {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, |     {file = "debugpy-1.8.6-cp311-cp311-win32.whl", hash = "sha256:cdaf0b9691879da2d13fa39b61c01887c34558d1ff6e5c30e2eb698f5384cd43"}, | ||||||
|     {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, |     {file = "debugpy-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:43996632bee7435583952155c06881074b9a742a86cee74e701d87ca532fe833"}, | ||||||
|     {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, |     {file = "debugpy-1.8.6-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:db891b141fc6ee4b5fc6d1cc8035ec329cabc64bdd2ae672b4550c87d4ecb128"}, | ||||||
|     {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, |     {file = "debugpy-1.8.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:567419081ff67da766c898ccf21e79f1adad0e321381b0dfc7a9c8f7a9347972"}, | ||||||
|     {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, |     {file = "debugpy-1.8.6-cp312-cp312-win32.whl", hash = "sha256:c9834dfd701a1f6bf0f7f0b8b1573970ae99ebbeee68314116e0ccc5c78eea3c"}, | ||||||
|     {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, |     {file = "debugpy-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:e4ce0570aa4aca87137890d23b86faeadf184924ad892d20c54237bcaab75d8f"}, | ||||||
|     {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, |     {file = "debugpy-1.8.6-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:df5dc9eb4ca050273b8e374a4cd967c43be1327eeb42bfe2f58b3cdfe7c68dcb"}, | ||||||
|     {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, |     {file = "debugpy-1.8.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a85707c6a84b0c5b3db92a2df685b5230dd8fb8c108298ba4f11dba157a615a"}, | ||||||
|     {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, |     {file = "debugpy-1.8.6-cp38-cp38-win32.whl", hash = "sha256:538c6cdcdcdad310bbefd96d7850be1cd46e703079cc9e67d42a9ca776cdc8a8"}, | ||||||
|     {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, |     {file = "debugpy-1.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:22140bc02c66cda6053b6eb56dfe01bbe22a4447846581ba1dd6df2c9f97982d"}, | ||||||
|     {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, |     {file = "debugpy-1.8.6-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:c1cef65cffbc96e7b392d9178dbfd524ab0750da6c0023c027ddcac968fd1caa"}, | ||||||
|     {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, |     {file = "debugpy-1.8.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e60bd06bb3cc5c0e957df748d1fab501e01416c43a7bdc756d2a992ea1b881"}, | ||||||
|     {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, |     {file = "debugpy-1.8.6-cp39-cp39-win32.whl", hash = "sha256:f7158252803d0752ed5398d291dee4c553bb12d14547c0e1843ab74ee9c31123"}, | ||||||
|     {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, |     {file = "debugpy-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3358aa619a073b620cd0d51d8a6176590af24abcc3fe2e479929a154bf591b51"}, | ||||||
|     {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, |     {file = "debugpy-1.8.6-py2.py3-none-any.whl", hash = "sha256:b48892df4d810eff21d3ef37274f4c60d32cdcafc462ad5647239036b0f0649f"}, | ||||||
|     {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, |     {file = "debugpy-1.8.6.zip", hash = "sha256:c931a9371a86784cee25dec8d65bc2dc7a21f3f1552e3833d9ef8f919d22280a"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "deepmerge" | name = "deepmerge" | ||||||
| version = "1.1.1" | version = "2.0" | ||||||
| description = "a toolset to deeply merge python dictionaries." | description = "A toolset for deeply merging Python dictionaries." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "deepmerge-1.1.1-py3-none-any.whl", hash = "sha256:7219dad9763f15be9dcd4bcb53e00f48e4eed6f5ed8f15824223eb934bb35977"}, |     {file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"}, | ||||||
|     {file = "deepmerge-1.1.1.tar.gz", hash = "sha256:53a489dc9449636e480a784359ae2aab3191748c920649551c8e378622f0eca4"}, |     {file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | dev = ["black", "build", "mypy", "pytest", "pyupgrade", "twine", "validate-pyproject[all]"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "defusedxml" | name = "defusedxml" | ||||||
| version = "0.7.1" | version = "0.7.1" | ||||||
| @ -1204,13 +1207,13 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django" | name = "django" | ||||||
| version = "5.0.8" | version = "5.0.9" | ||||||
| description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.10" | python-versions = ">=3.10" | ||||||
| files = [ | files = [ | ||||||
|     {file = "Django-5.0.8-py3-none-any.whl", hash = "sha256:333a7988f7ca4bc14d360d3d8f6b793704517761ae3813b95432043daec22a45"}, |     {file = "Django-5.0.9-py3-none-any.whl", hash = "sha256:f219576ba53be4e83f485130a7283f0efde06a9f2e3a7c3c5180327549f078fa"}, | ||||||
|     {file = "Django-5.0.8.tar.gz", hash = "sha256:ebe859c9da6fead9c9ee6dbfa4943b04f41342f4cea2c4d8c978ef0d10694f2b"}, |     {file = "Django-5.0.9.tar.gz", hash = "sha256:6333870d342329b60174da3a60dbd302e533f3b0bb0971516750e974a99b5a39"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -1284,13 +1287,13 @@ Django = ">=2.2" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django-model-utils" | name = "django-model-utils" | ||||||
| version = "4.5.1" | version = "5.0.0" | ||||||
| description = "Django model mixins and utilities" | description = "Django model mixins and utilities" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_model_utils-4.5.1-py3-none-any.whl", hash = "sha256:f1141fc71796242edeffed5ad53a8cc57f00d345eb5a3a63e3f69401cd562ee2"}, |     {file = "django_model_utils-5.0.0-py3-none-any.whl", hash = "sha256:fec78e6c323d565a221f7c4edc703f4567d7bb1caeafe1acd16a80c5ff82056b"}, | ||||||
|     {file = "django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a"}, |     {file = "django_model_utils-5.0.0.tar.gz", hash = "sha256:041cdd6230d2fbf6cd943e1969318bce762272077f4ecd333ab2263924b4e5eb"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -1312,13 +1315,13 @@ django = ">=3" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "django-pglock" | name = "django-pglock" | ||||||
| version = "1.6.0" | version = "1.6.2" | ||||||
| description = "Postgres locking routines and lock table access." | description = "Postgres locking routines and lock table access." | ||||||
| optional = false | optional = false | ||||||
| python-versions = "<4,>=3.8.0" | python-versions = "<4,>=3.8.0" | ||||||
| files = [ | files = [ | ||||||
|     {file = "django_pglock-1.6.0-py3-none-any.whl", hash = "sha256:41c98d0bd3738d11e6eaefcc3e5146028f118a593ac58c13d663b751170f01de"}, |     {file = "django_pglock-1.6.2-py3-none-any.whl", hash = "sha256:abdb92531bf8cb36471dc9eb33ed163b06bd3323140d132ed4f308b9e5505f50"}, | ||||||
|     {file = "django_pglock-1.6.0.tar.gz", hash = "sha256:724450ecc9886f39af599c477d84ad086545a5373215ef7a670cd25faca25a61"}, |     {file = "django_pglock-1.6.2.tar.gz", hash = "sha256:05db998cab21556d4a307eac4b5db8e50f874f42b1a581560b3c54610fee6a1b"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -1563,6 +1566,16 @@ files = [ | |||||||
| setuptools = "*" | setuptools = "*" | ||||||
| six = "*" | six = "*" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "durationpy" | ||||||
|  | version = "0.7" | ||||||
|  | description = "Module for converting between datetime.timedelta and Go's Duration strings." | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "durationpy-0.7.tar.gz", hash = "sha256:8447c43df4f1a0b434e70c15a38d77f5c9bd17284bfc1ff1d430f233d5083732"}, | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "email-validator" | name = "email-validator" | ||||||
| version = "2.2.0" | version = "2.2.0" | ||||||
| @ -1758,13 +1771,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "google-api-python-client" | name = "google-api-python-client" | ||||||
| version = "2.142.0" | version = "2.147.0" | ||||||
| description = "Google API Client Library for Python" | description = "Google API Client Library for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
|     {file = "google_api_python_client-2.142.0-py2.py3-none-any.whl", hash = "sha256:266799082bb8301f423ec204dffbffb470b502abbf29efd1f83e644d36eb5a8f"}, |     {file = "google_api_python_client-2.147.0-py2.py3-none-any.whl", hash = "sha256:c6ecfa193c695baa41e84562d8f8f244fcd164419eca3fc9fd7565646668f9b2"}, | ||||||
|     {file = "google_api_python_client-2.142.0.tar.gz", hash = "sha256:a1101ac9e24356557ca22f07ff48b7f61fa5d4b4e7feeef3bda16e5dcb86350e"}, |     {file = "google_api_python_client-2.147.0.tar.gz", hash = "sha256:e864c2cf61d34c00f05278b8bdb72b93b6fa34f0de9ead51d20435f3b65f91be"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -2047,13 +2060,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "importlib-metadata" | name = "importlib-metadata" | ||||||
| version = "8.0.0" | version = "8.4.0" | ||||||
| description = "Read metadata from Python packages" | description = "Read metadata from Python packages" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, |     {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, | ||||||
|     {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, |     {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -2240,17 +2253,18 @@ zookeeper = ["kazoo (>=2.8.0)"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "kubernetes" | name = "kubernetes" | ||||||
| version = "30.1.0" | version = "31.0.0" | ||||||
| description = "Kubernetes python client" | description = "Kubernetes python client" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
| files = [ | files = [ | ||||||
|     {file = "kubernetes-30.1.0-py2.py3-none-any.whl", hash = "sha256:e212e8b7579031dd2e512168b617373bc1e03888d41ac4e04039240a292d478d"}, |     {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, | ||||||
|     {file = "kubernetes-30.1.0.tar.gz", hash = "sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc"}, |     {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| certifi = ">=14.05.14" | certifi = ">=14.05.14" | ||||||
|  | durationpy = ">=0.7" | ||||||
| google-auth = ">=1.0.1" | google-auth = ">=1.0.1" | ||||||
| oauthlib = ">=3.2.2" | oauthlib = ">=3.2.2" | ||||||
| python-dateutil = ">=2.5.3" | python-dateutil = ">=2.5.3" | ||||||
| @ -2845,13 +2859,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "msgraph-sdk" | name = "msgraph-sdk" | ||||||
| version = "1.5.4" | version = "1.8.0" | ||||||
| description = "The Microsoft Graph Python SDK" | description = "The Microsoft Graph Python SDK" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "msgraph_sdk-1.5.4-py3-none-any.whl", hash = "sha256:9ea349f30cc4a03edb587e26554c7a4839a38c2ef30d4b5396882fd2be82dcac"}, |     {file = "msgraph_sdk-1.8.0-py3-none-any.whl", hash = "sha256:22a8e4a63f989865228f66a54501bef8105909c7156fe0a079ca9b5296339cc2"}, | ||||||
|     {file = "msgraph_sdk-1.5.4.tar.gz", hash = "sha256:b0e146328d136d1db175938d8fc901f3bb32acf3ea6fe93c0dc7c5a0abc45e39"}, |     {file = "msgraph_sdk-1.8.0.tar.gz", hash = "sha256:1ac84bd47ea288a84f46f6c6d0c89d164ee3453b917615632652344538098314"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -3025,49 +3039,49 @@ resolved_reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "opentelemetry-api" | name = "opentelemetry-api" | ||||||
| version = "1.26.0" | version = "1.27.0" | ||||||
| description = "OpenTelemetry Python API" | description = "OpenTelemetry Python API" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064"}, |     {file = "opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7"}, | ||||||
|     {file = "opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce"}, |     {file = "opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| deprecated = ">=1.2.6" | deprecated = ">=1.2.6" | ||||||
| importlib-metadata = ">=6.0,<=8.0.0" | importlib-metadata = ">=6.0,<=8.4.0" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "opentelemetry-sdk" | name = "opentelemetry-sdk" | ||||||
| version = "1.26.0" | version = "1.27.0" | ||||||
| description = "OpenTelemetry Python SDK" | description = "OpenTelemetry Python SDK" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897"}, |     {file = "opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d"}, | ||||||
|     {file = "opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85"}, |     {file = "opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| opentelemetry-api = "1.26.0" | opentelemetry-api = "1.27.0" | ||||||
| opentelemetry-semantic-conventions = "0.47b0" | opentelemetry-semantic-conventions = "0.48b0" | ||||||
| typing-extensions = ">=3.7.4" | typing-extensions = ">=3.7.4" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "opentelemetry-semantic-conventions" | name = "opentelemetry-semantic-conventions" | ||||||
| version = "0.47b0" | version = "0.48b0" | ||||||
| description = "OpenTelemetry Semantic Conventions" | description = "OpenTelemetry Semantic Conventions" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063"}, |     {file = "opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f"}, | ||||||
|     {file = "opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e"}, |     {file = "opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| deprecated = ">=1.2.6" | deprecated = ">=1.2.6" | ||||||
| opentelemetry-api = "1.26.0" | opentelemetry-api = "1.27.0" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "orjson" | name = "orjson" | ||||||
| @ -3158,13 +3172,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "paramiko" | name = "paramiko" | ||||||
| version = "3.4.1" | version = "3.5.0" | ||||||
| description = "SSH2 protocol library" | description = "SSH2 protocol library" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
| files = [ | files = [ | ||||||
|     {file = "paramiko-3.4.1-py3-none-any.whl", hash = "sha256:8e49fd2f82f84acf7ffd57c64311aa2b30e575370dc23bdb375b10262f7eac32"}, |     {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"}, | ||||||
|     {file = "paramiko-3.4.1.tar.gz", hash = "sha256:8b15302870af7f6652f2e038975c1d2973f06046cb5d7d65355668b3ecbece0c"}, |     {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -3201,13 +3215,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pdoc" | name = "pdoc" | ||||||
| version = "14.6.0" | version = "14.7.0" | ||||||
| description = "API Documentation for Python Projects" | description = "API Documentation for Python Projects" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "pdoc-14.6.0-py3-none-any.whl", hash = "sha256:36c42c546a317d8e3e8c0b39645f24161374de0c7066ccaae76628d721e49ba5"}, |     {file = "pdoc-14.7.0-py3-none-any.whl", hash = "sha256:72377a907efc6b2c5b3c56b717ef34f11d93621dced3b663f3aede0b844c0ad2"}, | ||||||
|     {file = "pdoc-14.6.0.tar.gz", hash = "sha256:6e98a24c5e0ca5d188397969cf82581836eaef13f172fc3820047bfe15c61c9a"}, |     {file = "pdoc-14.7.0.tar.gz", hash = "sha256:2d28af9c0acc39180744ad0543e4bbc3223ecba0d1302db315ec521c51f71f93"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -3434,36 +3448,36 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "psycopg" | name = "psycopg" | ||||||
| version = "3.2.1" | version = "3.2.3" | ||||||
| description = "PostgreSQL database adapter for Python" | description = "PostgreSQL database adapter for Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, |     {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, | ||||||
|     {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, |     {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| psycopg-c = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"c\""} | psycopg-c = {version = "3.2.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"c\""} | ||||||
| typing-extensions = ">=4.4" | typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} | ||||||
| tzdata = {version = "*", markers = "sys_platform == \"win32\""} | tzdata = {version = "*", markers = "sys_platform == \"win32\""} | ||||||
|  |  | ||||||
| [package.extras] | [package.extras] | ||||||
| binary = ["psycopg-binary (==3.2.1)"] | binary = ["psycopg-binary (==3.2.3)"] | ||||||
| c = ["psycopg-c (==3.2.1)"] | c = ["psycopg-c (==3.2.3)"] | ||||||
| dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.6)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] | dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] | ||||||
| docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] | docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] | ||||||
| pool = ["psycopg-pool"] | pool = ["psycopg-pool"] | ||||||
| test = ["anyio (>=4.0)", "mypy (>=1.6)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] | test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "psycopg-c" | name = "psycopg-c" | ||||||
| version = "3.2.1" | version = "3.2.3" | ||||||
| description = "PostgreSQL database adapter for Python -- C optimisation distribution" | description = "PostgreSQL database adapter for Python -- C optimisation distribution" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "psycopg_c-3.2.1.tar.gz", hash = "sha256:2d09943cc8a855c42c1e23b4298957b7ce8f27bf3683258c52fd139f601f7cda"}, |     {file = "psycopg_c-3.2.3.tar.gz", hash = "sha256:06ae7db8eaec1a3845960fa7f997f4ccdb1a7a7ab8dc593a680bcc74e1359671"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @ -3504,120 +3518,121 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pydantic" | name = "pydantic" | ||||||
| version = "2.8.2" | version = "2.9.2" | ||||||
| description = "Data validation using Python type hints" | description = "Data validation using Python type hints" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, |     {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, | ||||||
|     {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, |     {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| annotated-types = ">=0.4.0" | annotated-types = ">=0.6.0" | ||||||
| email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} | email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} | ||||||
| pydantic-core = "2.20.1" | pydantic-core = "2.23.4" | ||||||
| typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} | typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} | ||||||
|  |  | ||||||
| [package.extras] | [package.extras] | ||||||
| email = ["email-validator (>=2.0.0)"] | email = ["email-validator (>=2.0.0)"] | ||||||
|  | timezone = ["tzdata"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pydantic-core" | name = "pydantic-core" | ||||||
| version = "2.20.1" | version = "2.23.4" | ||||||
| description = "Core functionality for Pydantic validation and serialization" | description = "Core functionality for Pydantic validation and serialization" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, |     {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, |     {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, |     {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, |     {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, |     {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, |     {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, |     {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, |     {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, |     {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, |     {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, |     {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, |     {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, |     {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, |     {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, |     {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, |     {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, |     {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, | ||||||
|     {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, |     {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, |     {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, | ||||||
|     {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, |     {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, | ||||||
|     {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, |     {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -3761,13 +3776,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pytest" | name = "pytest" | ||||||
| version = "8.3.2" | version = "8.3.3" | ||||||
| description = "pytest: simple powerful testing with Python" | description = "pytest: simple powerful testing with Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, |     {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, | ||||||
|     {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, |     {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -3781,13 +3796,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pytest-django" | name = "pytest-django" | ||||||
| version = "4.8.0" | version = "4.9.0" | ||||||
| description = "A Django plugin for pytest." | description = "A Django plugin for pytest." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, |     {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, | ||||||
|     {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, |     {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4195,29 +4210,29 @@ pyasn1 = ">=0.1.3" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruff" | name = "ruff" | ||||||
| version = "0.6.2" | version = "0.6.8" | ||||||
| description = "An extremely fast Python linter and code formatter, written in Rust." | description = "An extremely fast Python linter and code formatter, written in Rust." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
|     {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, |     {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, |     {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, |     {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, |     {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, |     {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, |     {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, |     {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, |     {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, |     {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, |     {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"}, | ||||||
|     {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, |     {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"}, | ||||||
|     {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, |     {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @ -4256,13 +4271,13 @@ django-query = ["django (>=3.2)"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "selenium" | name = "selenium" | ||||||
| version = "4.23.1" | version = "4.25.0" | ||||||
| description = "Official Python bindings for Selenium WebDriver" | description = "Official Python bindings for Selenium WebDriver" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "selenium-4.23.1-py3-none-any.whl", hash = "sha256:3a8d9f23dc636bd3840dd56f00c2739e32ec0c1e34a821dd553e15babef24477"}, |     {file = "selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33"}, | ||||||
|     {file = "selenium-4.23.1.tar.gz", hash = "sha256:128d099e66284437e7128d2279176ec7a06e6ec7426e167f5d34987166bd8f46"}, |     {file = "selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4275,13 +4290,13 @@ websocket-client = ">=1.8,<2.0" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "sentry-sdk" | name = "sentry-sdk" | ||||||
| version = "2.13.0" | version = "2.14.0" | ||||||
| description = "Python client for Sentry (https://sentry.io)" | description = "Python client for Sentry (https://sentry.io)" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
| files = [ | files = [ | ||||||
|     {file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"}, |     {file = "sentry_sdk-2.14.0-py2.py3-none-any.whl", hash = "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4"}, | ||||||
|     {file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"}, |     {file = "sentry_sdk-2.14.0.tar.gz", hash = "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4652,13 +4667,13 @@ wsproto = ">=0.14" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "twilio" | name = "twilio" | ||||||
| version = "9.2.3" | version = "9.3.2" | ||||||
| description = "Twilio API client and TwiML generator" | description = "Twilio API client and TwiML generator" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7.0" | python-versions = ">=3.7.0" | ||||||
| files = [ | files = [ | ||||||
|     {file = "twilio-9.2.3-py2.py3-none-any.whl", hash = "sha256:76bfc39aa8d854510907cb7f9465814dfdea9e91ec199bb44f0785f05746f4cc"}, |     {file = "twilio-9.3.2-py2.py3-none-any.whl", hash = "sha256:7fcb2da241d2264b17fbab9ac0ca829c0f0abe23ce6db15d4bb0d4d2d583f953"}, | ||||||
|     {file = "twilio-9.2.3.tar.gz", hash = "sha256:da2255b5f3753cb3bf647fc6c50edbdb367ebc3cde6802806f6f863058a65f75"}, |     {file = "twilio-9.3.2.tar.gz", hash = "sha256:250fc6ce6960aa97a2e2ee7e718e3bc0e73d69731b97fe160ed2097f3cbeb5a8"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4669,13 +4684,13 @@ requests = ">=2.0.0" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "twisted" | name = "twisted" | ||||||
| version = "24.3.0" | version = "24.7.0" | ||||||
| description = "An asynchronous networking framework written in Python" | description = "An asynchronous networking framework written in Python" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8.0" | python-versions = ">=3.8.0" | ||||||
| files = [ | files = [ | ||||||
|     {file = "twisted-24.3.0-py3-none-any.whl", hash = "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63"}, |     {file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"}, | ||||||
|     {file = "twisted-24.3.0.tar.gz", hash = "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae"}, |     {file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4684,55 +4699,26 @@ automat = ">=0.8.0" | |||||||
| constantly = ">=15.1" | constantly = ">=15.1" | ||||||
| hyperlink = ">=17.1.1" | hyperlink = ">=17.1.1" | ||||||
| idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""} | idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""} | ||||||
| incremental = ">=22.10.0" | incremental = ">=24.7.0" | ||||||
| pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""} | pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""} | ||||||
| service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} | service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} | ||||||
| twisted-iocpsupport = {version = ">=1.0.2,<2", markers = "platform_system == \"Windows\""} |  | ||||||
| typing-extensions = ">=4.2.0" | typing-extensions = ">=4.2.0" | ||||||
| zope-interface = ">=5" | zope-interface = ">=5" | ||||||
|  |  | ||||||
| [package.extras] | [package.extras] | ||||||
| all-non-platform = ["twisted[conch,http2,serial,test,tls]", "twisted[conch,http2,serial,test,tls]"] | all-non-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] | ||||||
| conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"] | conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"] | ||||||
| dev = ["coverage (>=6b1,<7)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "twisted[dev-release]", "twistedchecker (>=0.7,<1.0)"] | dev = ["coverage (>=7.5,<8.0)", "cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pydoctor (>=23.9.0,<23.10.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "python-subunit (>=1.4,<2.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)"] | ||||||
| dev-release = ["pydoctor (>=23.9.0,<23.10.0)", "pydoctor (>=23.9.0,<23.10.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"] | dev-release = ["pydoctor (>=23.9.0,<23.10.0)", "pydoctor (>=23.9.0,<23.10.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"] | ||||||
| gtk-platform = ["pygobject", "pygobject", "twisted[all-non-platform]", "twisted[all-non-platform]"] | gtk-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pygobject", "pygobject", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] | ||||||
| http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] | http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] | ||||||
| macos-platform = ["pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "twisted[all-non-platform]", "twisted[all-non-platform]"] | macos-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] | ||||||
| mypy = ["mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "twisted[all-non-platform,dev]", "types-pyopenssl", "types-setuptools"] | mypy = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "coverage (>=7.5,<8.0)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "idna (>=2.4)", "mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "priority (>=1.1.0,<2.0)", "pydoctor (>=23.9.0,<23.10.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)", "types-pyopenssl", "types-setuptools"] | ||||||
| osx-platform = ["twisted[macos-platform]", "twisted[macos-platform]"] | osx-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)"] | ||||||
| serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] | serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] | ||||||
| test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] | test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] | ||||||
| tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"] | tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"] | ||||||
| windows-platform = ["pywin32 (!=226)", "pywin32 (!=226)", "twisted[all-non-platform]", "twisted[all-non-platform]"] | windows-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "twisted-iocpsupport (>=1.0.2)", "twisted-iocpsupport (>=1.0.2)"] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "twisted-iocpsupport" |  | ||||||
| version = "1.0.4" |  | ||||||
| description = "An extension for use in the twisted I/O Completion Ports reactor." |  | ||||||
| optional = false |  | ||||||
| python-versions = "*" |  | ||||||
| files = [ |  | ||||||
|     {file = "twisted-iocpsupport-1.0.4.tar.gz", hash = "sha256:858096c0d15e33f15ac157f455d8f86f2f2cdd223963e58c0f682a3af8362d89"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp310-cp310-win32.whl", hash = "sha256:afa2b630797f9ed2f27f3d9f55e3f72b4244911e45a8c82756f44babbf0b243e"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:0058c963c8957bcd3deda62122e89953c9de1e867a274facc9b15dde1a9f31e8"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp311-cp311-win32.whl", hash = "sha256:196f7c7ccad4ba4d1783b1c4e1d1b22d93c04275cd780bf7498d16c77319ad6e"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:4e5f97bcbabdd79cbaa969b63439b89801ea560f11d42b0a387634275c633623"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp312-cp312-win32.whl", hash = "sha256:6081bd7c2f4fcf9b383dcdb3b3385d75a26a7c9d2be25b6950c3d8ea652d2d2d"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:76f7e67cec1f1d097d1f4ed7de41be3d74546e1a4ede0c7d56e775c4dce5dfb0"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:3d306fc4d88a6bcf61ce9d572c738b918578121bfd72891625fab314549024b5"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:391ac4d6002a80e15f35adc4ad6056f4fe1c17ceb0d1f98ba01b0f4f917adfd7"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:0c1b5cf37f0b2d96cc3c9bc86fff16613b9f5d0ca565c96cf1f1fb8cfca4b81c"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:3c5dc11d72519e55f727320e3cee535feedfaee09c0f0765ed1ca7badff1ab3c"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp38-cp38-win32.whl", hash = "sha256:cc86c2ef598c15d824a243c2541c29459881c67fc3c0adb6efe2242f8f0ec3af"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c27985e949b9b1a1fb4c20c71d315c10ea0f93fdf3ccdd4a8c158b5926edd8c8"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp39-cp39-win32.whl", hash = "sha256:e311dfcb470696e3c077249615893cada598e62fa7c4e4ca090167bd2b7d331f"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4574eef1f3bb81501fb02f911298af3c02fe8179c31a33b361dd49180c3e644d"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:872747a3b64e2909aee59c803ccd0bceb9b75bf27915520ebd32d69687040fa2"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c2712b778bacf1db434e3e065adfed3db300754186a29aecac1efae9ef4bcaff"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7c66fa0aa4236b27b3c61cb488662d85dae746a6d1c7b0d91cf7aae118445adf"}, |  | ||||||
|     {file = "twisted_iocpsupport-1.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:300437af17396a945a58dcfffd77863303a8b6d9e65c6e81f1d2eed55b50d444"}, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "txaio" | name = "txaio" | ||||||
| @ -4796,13 +4782,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "urllib3" | name = "urllib3" | ||||||
| version = "2.2.2" | version = "2.2.3" | ||||||
| description = "HTTP library with thread-safe connection pooling, file post, and more." | description = "HTTP library with thread-safe connection pooling, file post, and more." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, |     {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, | ||||||
|     {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, |     {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4816,13 +4802,13 @@ zstd = ["zstandard (>=0.18.0)"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "uvicorn" | name = "uvicorn" | ||||||
| version = "0.30.6" | version = "0.31.0" | ||||||
| description = "The lightning-fast ASGI server." | description = "The lightning-fast ASGI server." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, |     {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"}, | ||||||
|     {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, |     {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -4896,46 +4882,41 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "watchdog" | name = "watchdog" | ||||||
| version = "4.0.2" | version = "5.0.3" | ||||||
| description = "Filesystem events monitoring" | description = "Filesystem events monitoring" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.9" | ||||||
| files = [ | files = [ | ||||||
|     {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"}, |     {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"}, | ||||||
|     {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"}, |     {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"}, | ||||||
|     {file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"}, |     {file = "watchdog-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b"}, | ||||||
|     {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"}, |     {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"}, | ||||||
|     {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"}, |     {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490"}, | ||||||
|     {file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"}, |     {file = "watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e"}, | ||||||
|     {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"}, |     {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8"}, | ||||||
|     {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"}, |     {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926"}, | ||||||
|     {file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"}, |     {file = "watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e"}, | ||||||
|     {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"}, |     {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7"}, | ||||||
|     {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"}, |     {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906"}, | ||||||
|     {file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"}, |     {file = "watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1"}, | ||||||
|     {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"}, |     {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3"}, | ||||||
|     {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"}, |     {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2"}, | ||||||
|     {file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"}, |     {file = "watchdog-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627"}, | ||||||
|     {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"}, |     {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7"}, | ||||||
|     {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"}, |     {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8"}, | ||||||
|     {file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"}, |     {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e"}, | ||||||
|     {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"}, |     {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b"}, | ||||||
|     {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91"}, | ||||||
|     {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c"}, | ||||||
|     {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c"}, | ||||||
|     {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221"}, | ||||||
|     {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"}, |     {file = "watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"}, |     {file = "watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"}, |     {file = "watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"}, |     {file = "watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"}, |     {file = "watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176"}, | ||||||
|     {file = "watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8"}, |  | ||||||
|     {file = "watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19"}, |  | ||||||
|     {file = "watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b"}, |  | ||||||
|     {file = "watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c"}, |  | ||||||
|     {file = "watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270"}, |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.extras] | [package.extras] | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2024.8.6" | version = "2024.8.3" | ||||||
| description = "" | description = "" | ||||||
| authors = ["authentik Team <hello@goauthentik.io>"] | authors = ["authentik Team <hello@goauthentik.io>"] | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										59
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2024.8.6 |   version: 2024.8.3 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
| @ -19493,6 +19493,10 @@ paths: | |||||||
|             format: uuid |             format: uuid | ||||||
|         explode: true |         explode: true | ||||||
|         style: form |         style: form | ||||||
|  |       - in: query | ||||||
|  |         name: redirect_uris | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|       - in: query |       - in: query | ||||||
|         name: refresh_token_validity |         name: refresh_token_validity | ||||||
|         schema: |         schema: | ||||||
| @ -19908,6 +19912,10 @@ paths: | |||||||
|             format: uuid |             format: uuid | ||||||
|         explode: true |         explode: true | ||||||
|         style: form |         style: form | ||||||
|  |       - in: query | ||||||
|  |         name: redirect_uris__iexact | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|       - name: search |       - name: search | ||||||
|         required: false |         required: false | ||||||
|         in: query |         in: query | ||||||
| @ -41657,11 +41665,6 @@ components: | |||||||
|       required: |       required: | ||||||
|       - challenge |       - challenge | ||||||
|       - name |       - name | ||||||
|     MatchingModeEnum: |  | ||||||
|       enum: |  | ||||||
|       - strict |  | ||||||
|       - regex |  | ||||||
|       type: string |  | ||||||
|     Metadata: |     Metadata: | ||||||
|       type: object |       type: object | ||||||
|       description: Serializer for blueprint metadata |       description: Serializer for blueprint metadata | ||||||
| @ -42350,9 +42353,8 @@ components: | |||||||
|           description: Key used to sign the tokens. Only required when JWT Algorithm |           description: Key used to sign the tokens. Only required when JWT Algorithm | ||||||
|             is set to RS256. |             is set to RS256. | ||||||
|         redirect_uris: |         redirect_uris: | ||||||
|           type: array |           type: string | ||||||
|           items: |           description: Enter each URI on a new line. | ||||||
|             $ref: '#/components/schemas/RedirectURI' |  | ||||||
|         sub_mode: |         sub_mode: | ||||||
|           allOf: |           allOf: | ||||||
|           - $ref: '#/components/schemas/SubModeEnum' |           - $ref: '#/components/schemas/SubModeEnum' | ||||||
| @ -42380,7 +42382,6 @@ components: | |||||||
|       - meta_model_name |       - meta_model_name | ||||||
|       - name |       - name | ||||||
|       - pk |       - pk | ||||||
|       - redirect_uris |  | ||||||
|       - verbose_name |       - verbose_name | ||||||
|       - verbose_name_plural |       - verbose_name_plural | ||||||
|     OAuth2ProviderRequest: |     OAuth2ProviderRequest: | ||||||
| @ -42443,9 +42444,8 @@ components: | |||||||
|           description: Key used to sign the tokens. Only required when JWT Algorithm |           description: Key used to sign the tokens. Only required when JWT Algorithm | ||||||
|             is set to RS256. |             is set to RS256. | ||||||
|         redirect_uris: |         redirect_uris: | ||||||
|           type: array |           type: string | ||||||
|           items: |           description: Enter each URI on a new line. | ||||||
|             $ref: '#/components/schemas/RedirectURIRequest' |  | ||||||
|         sub_mode: |         sub_mode: | ||||||
|           allOf: |           allOf: | ||||||
|           - $ref: '#/components/schemas/SubModeEnum' |           - $ref: '#/components/schemas/SubModeEnum' | ||||||
| @ -42466,7 +42466,6 @@ components: | |||||||
|       required: |       required: | ||||||
|       - authorization_flow |       - authorization_flow | ||||||
|       - name |       - name | ||||||
|       - redirect_uris |  | ||||||
|     OAuth2ProviderSetupURLs: |     OAuth2ProviderSetupURLs: | ||||||
|       type: object |       type: object | ||||||
|       description: OAuth2 Provider Metadata serializer |       description: OAuth2 Provider Metadata serializer | ||||||
| @ -46221,9 +46220,8 @@ components: | |||||||
|           description: Key used to sign the tokens. Only required when JWT Algorithm |           description: Key used to sign the tokens. Only required when JWT Algorithm | ||||||
|             is set to RS256. |             is set to RS256. | ||||||
|         redirect_uris: |         redirect_uris: | ||||||
|           type: array |           type: string | ||||||
|           items: |           description: Enter each URI on a new line. | ||||||
|             $ref: '#/components/schemas/RedirectURIRequest' |  | ||||||
|         sub_mode: |         sub_mode: | ||||||
|           allOf: |           allOf: | ||||||
|           - $ref: '#/components/schemas/SubModeEnum' |           - $ref: '#/components/schemas/SubModeEnum' | ||||||
| @ -48779,9 +48777,7 @@ components: | |||||||
|           description: When enabled, this provider will intercept the authorization |           description: When enabled, this provider will intercept the authorization | ||||||
|             header and authenticate requests based on its value. |             header and authenticate requests based on its value. | ||||||
|         redirect_uris: |         redirect_uris: | ||||||
|           type: array |           type: string | ||||||
|           items: |  | ||||||
|             $ref: '#/components/schemas/RedirectURI' |  | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         cookie_domain: |         cookie_domain: | ||||||
|           type: string |           type: string | ||||||
| @ -49361,29 +49357,6 @@ components: | |||||||
|           type: string |           type: string | ||||||
|       required: |       required: | ||||||
|       - to |       - to | ||||||
|     RedirectURI: |  | ||||||
|       type: object |  | ||||||
|       description: A single allowed redirect URI entry |  | ||||||
|       properties: |  | ||||||
|         matching_mode: |  | ||||||
|           $ref: '#/components/schemas/MatchingModeEnum' |  | ||||||
|         url: |  | ||||||
|           type: string |  | ||||||
|       required: |  | ||||||
|       - matching_mode |  | ||||||
|       - url |  | ||||||
|     RedirectURIRequest: |  | ||||||
|       type: object |  | ||||||
|       description: A single allowed redirect URI entry |  | ||||||
|       properties: |  | ||||||
|         matching_mode: |  | ||||||
|           $ref: '#/components/schemas/MatchingModeEnum' |  | ||||||
|         url: |  | ||||||
|           type: string |  | ||||||
|           minLength: 1 |  | ||||||
|       required: |  | ||||||
|       - matching_mode |  | ||||||
|       - url |  | ||||||
|     Reputation: |     Reputation: | ||||||
|       type: object |       type: object | ||||||
|       description: Reputation Serializer |       description: Reputation Serializer | ||||||
|  | |||||||
| @ -13,12 +13,7 @@ from authentik.flows.models import Flow | |||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| from authentik.policies.expression.models import ExpressionPolicy | from authentik.policies.expression.models import ExpressionPolicy | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider | ||||||
|     ClientTypes, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
| ) |  | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -84,9 +79,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/github", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github") |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         Application.objects.create( |         Application.objects.create( | ||||||
| @ -141,9 +134,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/github", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github") |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
| @ -214,9 +205,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/github", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github") |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|  | |||||||
| @ -20,13 +20,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_EMAIL, |     SCOPE_OPENID_EMAIL, | ||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
|     ClientTypes, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -93,7 +87,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/")], |             redirect_uris="http://localhost:3000/", | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -142,11 +136,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|                 RedirectURI( |  | ||||||
|                     RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -208,11 +198,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|                 RedirectURI( |  | ||||||
|                     RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -284,11 +270,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|                 RedirectURI( |  | ||||||
|                     RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
| @ -368,11 +350,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|                 RedirectURI( |  | ||||||
|                     RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
|  | |||||||
| @ -21,13 +21,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_EMAIL, |     SCOPE_OPENID_EMAIL, | ||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
|     ClientTypes, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -79,7 +73,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")], |             redirect_uris="http://localhost:9009/", | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -128,9 +122,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:9009/auth/callback", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback") |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -171,7 +163,6 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|  |  | ||||||
|         self.assertEqual(body["IDTokenClaims"]["nickname"], self.user.username) |         self.assertEqual(body["IDTokenClaims"]["nickname"], self.user.username) | ||||||
|         self.assertEqual(body["IDTokenClaims"]["amr"], ["pwd"]) |  | ||||||
|         self.assertEqual(body["UserInfo"]["nickname"], self.user.username) |         self.assertEqual(body["UserInfo"]["nickname"], self.user.username) | ||||||
|  |  | ||||||
|         self.assertEqual(body["IDTokenClaims"]["name"], self.user.name) |         self.assertEqual(body["IDTokenClaims"]["name"], self.user.name) | ||||||
| @ -202,9 +193,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:9009/auth/callback", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback") |  | ||||||
|             ], |  | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
| @ -275,9 +264,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:9009/auth/callback", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback") |  | ||||||
|             ], |  | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
|  | |||||||
| @ -21,13 +21,7 @@ from authentik.providers.oauth2.constants import ( | |||||||
|     SCOPE_OPENID_EMAIL, |     SCOPE_OPENID_EMAIL, | ||||||
|     SCOPE_OPENID_PROFILE, |     SCOPE_OPENID_PROFILE, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.models import ( | from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping | ||||||
|     ClientTypes, |  | ||||||
|     OAuth2Provider, |  | ||||||
|     RedirectURI, |  | ||||||
|     RedirectURIMatchingMode, |  | ||||||
|     ScopeMapping, |  | ||||||
| ) |  | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.e2e.utils import SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -80,7 +74,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")], |             redirect_uris="http://localhost:9009/", | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -129,9 +123,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:9009/implicit/", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/") |  | ||||||
|             ], |  | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
| @ -184,9 +176,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:9009/implicit/", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/") |  | ||||||
|             ], |  | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
| @ -254,9 +244,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | |||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|             redirect_uris=[ |             redirect_uris="http://localhost:9009/implicit/", | ||||||
|                 RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/") |  | ||||||
|             ], |  | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ | |||||||
|  |  | ||||||
| from base64 import b64encode | from base64 import b64encode | ||||||
| from dataclasses import asdict | from dataclasses import asdict | ||||||
| from json import loads |  | ||||||
| from sys import platform | from sys import platform | ||||||
| from time import sleep | from time import sleep | ||||||
| from typing import Any | from typing import Any | ||||||
| @ -11,7 +10,6 @@ from unittest.case import skip, skipUnless | |||||||
| from channels.testing import ChannelsLiveServerTestCase | from channels.testing import ChannelsLiveServerTestCase | ||||||
| from docker.client import DockerClient, from_env | from docker.client import DockerClient, from_env | ||||||
| from docker.models.containers import Container | from docker.models.containers import Container | ||||||
| from jwt import decode |  | ||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import apply_blueprint, reconcile_app | from authentik.blueprints.tests import apply_blueprint, reconcile_app | ||||||
| @ -117,15 +115,8 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         sleep(1) |         sleep(1) | ||||||
|  |  | ||||||
|         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text |         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text | ||||||
|         body = loads(full_body_text) |         self.assertIn(f"X-Authentik-Username: {self.user.username}", full_body_text) | ||||||
|  |         self.assertIn("X-Foo: bar", full_body_text) | ||||||
|         self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) |  | ||||||
|         self.assertEqual(body["headers"]["X-Foo"], ["bar"]) |  | ||||||
|         raw_jwt: str = body["headers"]["X-Authentik-Jwt"][0] |  | ||||||
|         jwt = decode(raw_jwt, options={"verify_signature": False}) |  | ||||||
|  |  | ||||||
|         self.assertIsNotNone(jwt["sid"]) |  | ||||||
|         self.assertIsNotNone(jwt["ak_proxy"]) |  | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") |         self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") | ||||||
|         sleep(2) |         sleep(2) | ||||||
|  | |||||||
| @ -1,4 +0,0 @@ | |||||||
| # don't ever lint node_modules |  | ||||||
| node_modules |  | ||||||
| # don't lint nyc coverage output |  | ||||||
| coverage |  | ||||||
| @ -1,29 +0,0 @@ | |||||||
| { |  | ||||||
|     "env": { |  | ||||||
|         "browser": true, |  | ||||||
|         "es2021": true |  | ||||||
|     }, |  | ||||||
|     "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], |  | ||||||
|     "parser": "@typescript-eslint/parser", |  | ||||||
|     "parserOptions": { |  | ||||||
|         "ecmaVersion": 12, |  | ||||||
|         "sourceType": "module" |  | ||||||
|     }, |  | ||||||
|     "plugins": ["@typescript-eslint"], |  | ||||||
|     "rules": { |  | ||||||
|         "indent": "off", |  | ||||||
|         "linebreak-style": ["error", "unix"], |  | ||||||
|         "quotes": ["error", "double", { "avoidEscape": true }], |  | ||||||
|         "semi": ["error", "always"], |  | ||||||
|         "@typescript-eslint/ban-ts-comment": "off", |  | ||||||
|         "no-unused-vars": "off", |  | ||||||
|         "@typescript-eslint/no-unused-vars": [ |  | ||||||
|             "error", |  | ||||||
|             { |  | ||||||
|                 "argsIgnorePattern": "^_", |  | ||||||
|                 "varsIgnorePattern": "^_", |  | ||||||
|                 "caughtErrorsIgnorePattern": "^_" |  | ||||||
|             } |  | ||||||
|         ] |  | ||||||
|     } |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	