Compare commits
	
		
			234 Commits
		
	
	
		
			version/20
			...
			analytics
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 435ba598bb | |||
| 582511abcc | |||
| 80ea1dae81 | |||
| 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.6.4
 | 
					current_version = 2024.8.2
 | 
				
			||||||
tag = True
 | 
					tag = True
 | 
				
			||||||
commit = True
 | 
					commit = True
 | 
				
			||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
 | 
					parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
 | 
				
			||||||
 | 
				
			|||||||
@ -29,9 +29,9 @@ outputs:
 | 
				
			|||||||
  imageTags:
 | 
					  imageTags:
 | 
				
			||||||
    description: "Docker image tags"
 | 
					    description: "Docker image tags"
 | 
				
			||||||
    value: ${{ steps.ev.outputs.imageTags }}
 | 
					    value: ${{ steps.ev.outputs.imageTags }}
 | 
				
			||||||
  imageNames:
 | 
					  attestImageNames:
 | 
				
			||||||
    description: "Docker image names"
 | 
					    description: "Docker image names used for attestation"
 | 
				
			||||||
    value: ${{ steps.ev.outputs.imageNames }}
 | 
					    value: ${{ steps.ev.outputs.attestImageNames }}
 | 
				
			||||||
  imageMainTag:
 | 
					  imageMainTag:
 | 
				
			||||||
    description: "Docker image main tag"
 | 
					    description: "Docker image main tag"
 | 
				
			||||||
    value: ${{ steps.ev.outputs.imageMainTag }}
 | 
					    value: ${{ steps.ev.outputs.imageMainTag }}
 | 
				
			||||||
 | 
				
			|||||||
@ -51,15 +51,24 @@ else:
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
image_main_tag = image_tags[0].split(":")[-1]
 | 
					image_main_tag = image_tags[0].split(":")[-1]
 | 
				
			||||||
image_tags_rendered = ",".join(image_tags)
 | 
					
 | 
				
			||||||
image_names_rendered = ",".join(set(name.split(":")[0] for name in image_tags))
 | 
					
 | 
				
			||||||
 | 
					def get_attest_image_names(image_with_tags: list[str]):
 | 
				
			||||||
 | 
					    """Attestation only for GHCR"""
 | 
				
			||||||
 | 
					    image_tags = []
 | 
				
			||||||
 | 
					    for image_name in set(name.split(":")[0] for name in image_with_tags):
 | 
				
			||||||
 | 
					        if not image_name.startswith("ghcr.io"):
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        image_tags.append(image_name)
 | 
				
			||||||
 | 
					    return ",".join(set(image_tags))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
 | 
					with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
 | 
				
			||||||
    print(f"shouldBuild={should_build}", file=_output)
 | 
					    print(f"shouldBuild={should_build}", file=_output)
 | 
				
			||||||
    print(f"sha={sha}", file=_output)
 | 
					    print(f"sha={sha}", file=_output)
 | 
				
			||||||
    print(f"version={version}", file=_output)
 | 
					    print(f"version={version}", file=_output)
 | 
				
			||||||
    print(f"prerelease={prerelease}", file=_output)
 | 
					    print(f"prerelease={prerelease}", file=_output)
 | 
				
			||||||
    print(f"imageTags={image_tags_rendered}", file=_output)
 | 
					    print(f"imageTags={','.join(image_tags)}", file=_output)
 | 
				
			||||||
    print(f"imageNames={image_names_rendered}", file=_output)
 | 
					    print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
 | 
				
			||||||
    print(f"imageMainTag={image_main_tag}", file=_output)
 | 
					    print(f"imageMainTag={image_main_tag}", file=_output)
 | 
				
			||||||
    print(f"imageMainName={image_tags[0]}", file=_output)
 | 
					    print(f"imageMainName={image_tags[0]}", file=_output)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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 }}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.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
 | 
				
			||||||
@ -261,7 +279,7 @@ jobs:
 | 
				
			|||||||
        id: attest
 | 
					        id: attest
 | 
				
			||||||
        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
					        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          subject-name: ${{ steps.ev.outputs.imageNames }}
 | 
					          subject-name: ${{ steps.ev.outputs.attestImageNames }}
 | 
				
			||||||
          subject-digest: ${{ steps.push.outputs.digest }}
 | 
					          subject-digest: ${{ steps.push.outputs.digest }}
 | 
				
			||||||
          push-to-registry: true
 | 
					          push-to-registry: true
 | 
				
			||||||
  pr-comment:
 | 
					  pr-comment:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -115,7 +115,7 @@ jobs:
 | 
				
			|||||||
        id: attest
 | 
					        id: attest
 | 
				
			||||||
        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
					        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          subject-name: ${{ steps.ev.outputs.imageNames }}
 | 
					          subject-name: ${{ steps.ev.outputs.attestImageNames }}
 | 
				
			||||||
          subject-digest: ${{ steps.push.outputs.digest }}
 | 
					          subject-digest: ${{ steps.push.outputs.digest }}
 | 
				
			||||||
          push-to-registry: true
 | 
					          push-to-registry: true
 | 
				
			||||||
  build-binary:
 | 
					  build-binary:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -58,7 +58,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/attest-build-provenance@v1
 | 
					      - uses: actions/attest-build-provenance@v1
 | 
				
			||||||
        id: attest
 | 
					        id: attest
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          subject-name: ${{ steps.ev.outputs.imageNames }}
 | 
					          subject-name: ${{ steps.ev.outputs.attestImageNames }}
 | 
				
			||||||
          subject-digest: ${{ steps.push.outputs.digest }}
 | 
					          subject-digest: ${{ steps.push.outputs.digest }}
 | 
				
			||||||
          push-to-registry: true
 | 
					          push-to-registry: true
 | 
				
			||||||
  build-outpost:
 | 
					  build-outpost:
 | 
				
			||||||
@ -122,7 +122,7 @@ jobs:
 | 
				
			|||||||
      - uses: actions/attest-build-provenance@v1
 | 
					      - uses: actions/attest-build-provenance@v1
 | 
				
			||||||
        id: attest
 | 
					        id: attest
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          subject-name: ${{ steps.ev.outputs.imageNames }}
 | 
					          subject-name: ${{ steps.ev.outputs.attestImageNames }}
 | 
				
			||||||
          subject-digest: ${{ steps.push.outputs.digest }}
 | 
					          subject-digest: ${{ steps.push.outputs.digest }}
 | 
				
			||||||
          push-to-registry: true
 | 
					          push-to-registry: true
 | 
				
			||||||
  build-outpost-binary:
 | 
					  build-outpost-binary:
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							@ -205,7 +205,7 @@ gen: gen-build gen-client-ts
 | 
				
			|||||||
web-build: web-install  ## Build the Authentik UI
 | 
					web-build: web-install  ## Build the Authentik UI
 | 
				
			||||||
	cd web && npm run build
 | 
						cd web && npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
web: web-lint-fix web-lint web-check-compile web-test  ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
 | 
					web: web-lint-fix web-lint web-check-compile  ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
web-install:  ## Install the necessary libraries to build the Authentik UI
 | 
					web-install:  ## Install the necessary libraries to build the Authentik UI
 | 
				
			||||||
	cd web && npm ci
 | 
						cd web && npm ci
 | 
				
			||||||
 | 
				
			|||||||
@ -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.6.4"
 | 
					__version__ = "2024.8.2"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								authentik/admin/analytics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								authentik/admin/analytics.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					"""authentik admin analytics"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_description() -> dict[str, str]:
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        "worker_count": _("Number of running workers"),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_data() -> dict[str, Any]:
 | 
				
			||||||
 | 
					    worker_count = len(CELERY_APP.control.ping(timeout=0.5))
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        "worker_count": worker_count,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								authentik/analytics/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/analytics/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										54
									
								
								authentik/analytics/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								authentik/analytics/api.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					"""authentik analytics api"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from drf_spectacular.utils import extend_schema, inline_serializer
 | 
				
			||||||
 | 
					from rest_framework.fields import CharField, DictField
 | 
				
			||||||
 | 
					from rest_framework.request import Request
 | 
				
			||||||
 | 
					from rest_framework.response import Response
 | 
				
			||||||
 | 
					from rest_framework.viewsets import ViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.analytics.utils import get_analytics_data, get_analytics_description
 | 
				
			||||||
 | 
					from authentik.core.api.utils import PassiveSerializer
 | 
				
			||||||
 | 
					from authentik.rbac.permissions import HasPermission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AnalyticsDescriptionSerializer(PassiveSerializer):
 | 
				
			||||||
 | 
					    label = CharField()
 | 
				
			||||||
 | 
					    desc = CharField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AnalyticsDescriptionViewSet(ViewSet):
 | 
				
			||||||
 | 
					    """Read-only view of analytics descriptions"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    permission_classes = [HasPermission("authentik_rbac.view_system_settings")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(responses={200: AnalyticsDescriptionSerializer})
 | 
				
			||||||
 | 
					    def list(self, request: Request) -> Response:
 | 
				
			||||||
 | 
					        """Read-only view of analytics descriptions"""
 | 
				
			||||||
 | 
					        data = []
 | 
				
			||||||
 | 
					        for label, desc in get_analytics_description().items():
 | 
				
			||||||
 | 
					            data.append({"label": label, "desc": desc})
 | 
				
			||||||
 | 
					        return Response(AnalyticsDescriptionSerializer(data, many=True).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AnalyticsDataViewSet(ViewSet):
 | 
				
			||||||
 | 
					    """Read-only view of analytics descriptions"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    permission_classes = [HasPermission("authentik_rbac.edit_system_settings")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        responses={
 | 
				
			||||||
 | 
					            200: inline_serializer(
 | 
				
			||||||
 | 
					                name="AnalyticsData",
 | 
				
			||||||
 | 
					                fields={
 | 
				
			||||||
 | 
					                    "data": DictField(),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def list(self, request: Request) -> Response:
 | 
				
			||||||
 | 
					        """Read-only view of analytics descriptions"""
 | 
				
			||||||
 | 
					        return Response(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "data": get_analytics_data(force=True),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
							
								
								
									
										12
									
								
								authentik/analytics/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								authentik/analytics/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					"""authentik analytics app config"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.blueprints.apps import ManagedAppConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AuthentikAdminConfig(ManagedAppConfig):
 | 
				
			||||||
 | 
					    """authentik analytics app config"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name = "authentik.analytics"
 | 
				
			||||||
 | 
					    label = "authentik_analytics"
 | 
				
			||||||
 | 
					    verbose_name = "authentik Analytics"
 | 
				
			||||||
 | 
					    default = True
 | 
				
			||||||
							
								
								
									
										19
									
								
								authentik/analytics/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								authentik/analytics/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					"""authentik analytics mixins"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AnalyticsMixin:
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_analytics_description(cls) -> dict[str, str]:
 | 
				
			||||||
 | 
					        object_name = _(cls._meta.verbose_name)
 | 
				
			||||||
 | 
					        count_desc = _("Number of {object_name} objects".format_map({"object_name": object_name}))
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "count": count_desc,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_analytics_data(cls) -> dict[str, Any]:
 | 
				
			||||||
 | 
					        return {"count": cls.objects.all().count()}
 | 
				
			||||||
							
								
								
									
										17
									
								
								authentik/analytics/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								authentik/analytics/settings.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					"""authentik admin settings"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from celery.schedules import crontab
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.lib.utils.time import fqdn_rand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CELERY_BEAT_SCHEDULE = {
 | 
				
			||||||
 | 
					    "analytics_send": {
 | 
				
			||||||
 | 
					        "task": "authentik.analytics.tasks.send_analytics",
 | 
				
			||||||
 | 
					        "schedule": crontab(
 | 
				
			||||||
 | 
					            minute=fqdn_rand("analytics_send"),
 | 
				
			||||||
 | 
					            hour=fqdn_rand("analytics_send", stop=24),
 | 
				
			||||||
 | 
					            day_of_week=fqdn_rand("analytics_send", 7),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        "options": {"queue": "authentik_scheduled"},
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								authentik/analytics/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								authentik/analytics/tasks.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					"""authentik admin tasks"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import orjson
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					from requests import RequestException
 | 
				
			||||||
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.analytics.utils import get_analytics_data
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
 | 
				
			||||||
 | 
					from authentik.lib.utils.http import get_http_session
 | 
				
			||||||
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					from authentik.tenants.models import Tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CELERY_APP.task(bind=True, base=SystemTask)
 | 
				
			||||||
 | 
					@prefill_task
 | 
				
			||||||
 | 
					def send_analytics(self: SystemTask):
 | 
				
			||||||
 | 
					    """Send analytics"""
 | 
				
			||||||
 | 
					    for tenant in Tenant.objects.filter(ready=True):
 | 
				
			||||||
 | 
					        data = get_analytics_data(current_tenant=tenant)
 | 
				
			||||||
 | 
					        if not tenant.analytics_enabled or not data:
 | 
				
			||||||
 | 
					            self.set_status(TaskStatus.WARNING, "Analytics disabled. Nothing was sent.")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            response = get_http_session().post(
 | 
				
			||||||
 | 
					                "https://customers.goauthentik.io/api/analytics/post/", json=data
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            response.raise_for_status()
 | 
				
			||||||
 | 
					            self.set_status(
 | 
				
			||||||
 | 
					                TaskStatus.SUCCESSFUL,
 | 
				
			||||||
 | 
					                "Successfully sent analytics",
 | 
				
			||||||
 | 
					                orjson.dumps(
 | 
				
			||||||
 | 
					                    data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS | orjson.OPT_UTC_Z
 | 
				
			||||||
 | 
					                ).decode(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            Event.new(
 | 
				
			||||||
 | 
					                EventAction.ANALYTICS_SENT,
 | 
				
			||||||
 | 
					                message=_("Analytics sent"),
 | 
				
			||||||
 | 
					                analytics_data=data,
 | 
				
			||||||
 | 
					            ).save()
 | 
				
			||||||
 | 
					        except (RequestException, IndexError) as exc:
 | 
				
			||||||
 | 
					            self.set_error(exc)
 | 
				
			||||||
							
								
								
									
										76
									
								
								authentik/analytics/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								authentik/analytics/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					"""authentik analytics tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from json import loads
 | 
				
			||||||
 | 
					from requests_mock import Mocker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik import __version__
 | 
				
			||||||
 | 
					from authentik.analytics.tasks import send_analytics
 | 
				
			||||||
 | 
					from authentik.analytics.utils import get_analytics_apps_data, get_analytics_apps_description, get_analytics_data, get_analytics_description, get_analytics_models_data, get_analytics_models_description
 | 
				
			||||||
 | 
					from authentik.core.models import Group, User
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
 | 
					from authentik.tenants.utils import get_current_tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestAnalytics(TestCase):
 | 
				
			||||||
 | 
					    """test analytics api"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
 | 
					        super().setUp()
 | 
				
			||||||
 | 
					        self.user = User.objects.create(username=generate_id())
 | 
				
			||||||
 | 
					        self.group = Group.objects.create(name=generate_id(), is_superuser=True)
 | 
				
			||||||
 | 
					        self.group.users.add(self.user)
 | 
				
			||||||
 | 
					        self.client.force_login(self.user)
 | 
				
			||||||
 | 
					        self.tenant = get_current_tenant()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_description_api(self):
 | 
				
			||||||
 | 
					        """Test Version API"""
 | 
				
			||||||
 | 
					        response = self.client.get(reverse("authentik_api:analytics-description-list"))
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					        loads(response.content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_data_api(self):
 | 
				
			||||||
 | 
					        """Test Version API"""
 | 
				
			||||||
 | 
					        response = self.client.get(reverse("authentik_api:analytics-data-list"))
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					        body = loads(response.content)
 | 
				
			||||||
 | 
					        self.assertEqual(body["data"]["version"], __version__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_sending_enabled(self):
 | 
				
			||||||
 | 
					        """Test analytics sending"""
 | 
				
			||||||
 | 
					        self.tenant.analytics_enabled = True
 | 
				
			||||||
 | 
					        self.tenant.save()
 | 
				
			||||||
 | 
					        with Mocker() as mocker:
 | 
				
			||||||
 | 
					            mocker.post("https://customers.goauthentik.io/api/analytics/post/", status_code=200)
 | 
				
			||||||
 | 
					            send_analytics.delay().get()
 | 
				
			||||||
 | 
					            self.assertTrue(
 | 
				
			||||||
 | 
					                Event.objects.filter(
 | 
				
			||||||
 | 
					                    action=EventAction.ANALYTICS_SENT
 | 
				
			||||||
 | 
					                ).exists()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_sending_disabled(self):
 | 
				
			||||||
 | 
					        """Test analytics sending"""
 | 
				
			||||||
 | 
					        self.tenant.analytics_enabled = False
 | 
				
			||||||
 | 
					        self.tenant.save()
 | 
				
			||||||
 | 
					        send_analytics.delay().get()
 | 
				
			||||||
 | 
					        self.assertFalse(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.ANALYTICS_SENT
 | 
				
			||||||
 | 
					            ).exists()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_description_data_match_apps(self):
 | 
				
			||||||
 | 
					        """Test description and data keys match"""
 | 
				
			||||||
 | 
					        description = get_analytics_apps_description()
 | 
				
			||||||
 | 
					        data = get_analytics_apps_data()
 | 
				
			||||||
 | 
					        self.assertEqual(data.keys(), description.keys())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_description_data_match_models(self):
 | 
				
			||||||
 | 
					        """Test description and data keys match"""
 | 
				
			||||||
 | 
					        description = get_analytics_models_description()
 | 
				
			||||||
 | 
					        data = get_analytics_models_data()
 | 
				
			||||||
 | 
					        self.assertEqual(data.keys(), description.keys())
 | 
				
			||||||
							
								
								
									
										8
									
								
								authentik/analytics/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								authentik/analytics/urls.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					"""API URLs"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.analytics.api import AnalyticsDataViewSet, AnalyticsDescriptionViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					api_urlpatterns = [
 | 
				
			||||||
 | 
					    ("analytics/description", AnalyticsDescriptionViewSet, "analytics-description"),
 | 
				
			||||||
 | 
					    ("analytics/data", AnalyticsDataViewSet, "analytics-data"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										112
									
								
								authentik/analytics/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								authentik/analytics/utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					"""authentik analytics utils"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from hashlib import sha256
 | 
				
			||||||
 | 
					from importlib import import_module
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik import get_full_version
 | 
				
			||||||
 | 
					from authentik.analytics.models import AnalyticsMixin
 | 
				
			||||||
 | 
					from authentik.lib.utils.reflection import get_apps
 | 
				
			||||||
 | 
					from authentik.root.install_id import get_install_id
 | 
				
			||||||
 | 
					from authentik.tenants.models import Tenant
 | 
				
			||||||
 | 
					from authentik.tenants.utils import get_current_tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_apps() -> dict:
 | 
				
			||||||
 | 
					    modules = {}
 | 
				
			||||||
 | 
					    for _authentik_app in get_apps():
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            module = import_module(f"{_authentik_app.name}.analytics")
 | 
				
			||||||
 | 
					        except ModuleNotFoundError:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        except ImportError as exc:
 | 
				
			||||||
 | 
					            LOGGER.warning(
 | 
				
			||||||
 | 
					                "Could not import app's analytics", app_name=_authentik_app.name, exc=exc
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        if not hasattr(module, "get_analytics_description") or not hasattr(
 | 
				
			||||||
 | 
					            module, "get_analytics_data"
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            LOGGER.debug(
 | 
				
			||||||
 | 
					                "App does not define API URLs",
 | 
				
			||||||
 | 
					                app_name=_authentik_app.name,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        modules[_authentik_app.label] = module
 | 
				
			||||||
 | 
					    return modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_apps_description() -> dict[str, str]:
 | 
				
			||||||
 | 
					    result = {}
 | 
				
			||||||
 | 
					    for app_label, module in get_analytics_apps().items():
 | 
				
			||||||
 | 
					        for k, v in module.get_analytics_description().items():
 | 
				
			||||||
 | 
					            result[f"{app_label}/app/{k}"] = v
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_apps_data() -> dict[str, Any]:
 | 
				
			||||||
 | 
					    result = {}
 | 
				
			||||||
 | 
					    for app_label, module in get_analytics_apps().items():
 | 
				
			||||||
 | 
					        for k, v in module.get_analytics_data().items():
 | 
				
			||||||
 | 
					            result[f"{app_label}/app/{k}"] = v
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_models() -> list[AnalyticsMixin]:
 | 
				
			||||||
 | 
					    def get_subclasses(cls):
 | 
				
			||||||
 | 
					        for subclass in cls.__subclasses__():
 | 
				
			||||||
 | 
					            if subclass.__subclasses__():
 | 
				
			||||||
 | 
					                yield from get_subclasses(subclass)
 | 
				
			||||||
 | 
					            elif not subclass._meta.abstract:
 | 
				
			||||||
 | 
					                yield subclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return list(get_subclasses(AnalyticsMixin))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_models_description() -> dict[str, str]:
 | 
				
			||||||
 | 
					    result = {}
 | 
				
			||||||
 | 
					    for model in get_analytics_models():
 | 
				
			||||||
 | 
					        for k, v in model.get_analytics_description().items():
 | 
				
			||||||
 | 
					            result[f"{model._meta.app_label}/models/{model._meta.object_name}/{k}"] = v
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_models_data() -> dict[str, Any]:
 | 
				
			||||||
 | 
					    result = {}
 | 
				
			||||||
 | 
					    for model in get_analytics_models():
 | 
				
			||||||
 | 
					        for k, v in model.get_analytics_data().items():
 | 
				
			||||||
 | 
					            result[f"{model._meta.app_label}/models/{model._meta.object_name}/{k}"] = v
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_description() -> dict[str, str]:
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        **get_analytics_apps_description(),
 | 
				
			||||||
 | 
					        **get_analytics_models_description(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_analytics_data(current_tenant: Tenant | None = None, force: bool = False) -> dict[str, Any]:
 | 
				
			||||||
 | 
					    current_tenant = current_tenant or get_current_tenant()
 | 
				
			||||||
 | 
					    if not current_tenant.analytics_enabled and not force:
 | 
				
			||||||
 | 
					        return {}
 | 
				
			||||||
 | 
					    data = {
 | 
				
			||||||
 | 
					        **get_analytics_apps_data(),
 | 
				
			||||||
 | 
					        **get_analytics_models_data(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    to_remove = []
 | 
				
			||||||
 | 
					    for key in data.keys():
 | 
				
			||||||
 | 
					        if key not in current_tenant.analytics_sources:
 | 
				
			||||||
 | 
					            to_remove.append(key)
 | 
				
			||||||
 | 
					    for key in to_remove:
 | 
				
			||||||
 | 
					        del data[key]
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        **data,
 | 
				
			||||||
 | 
					        "install_id_hash": sha256(get_install_id().encode()).hexdigest(),
 | 
				
			||||||
 | 
					        "tenant_hash": sha256(current_tenant.tenant_uuid.bytes).hexdigest(),
 | 
				
			||||||
 | 
					        "version": get_full_version(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
@ -30,8 +30,10 @@ from authentik.core.api.utils import (
 | 
				
			|||||||
    PassiveSerializer,
 | 
					    PassiveSerializer,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.core.expression.evaluator import PropertyMappingEvaluator
 | 
					from authentik.core.expression.evaluator import PropertyMappingEvaluator
 | 
				
			||||||
 | 
					from authentik.core.expression.exceptions import PropertyMappingExpressionException
 | 
				
			||||||
from authentik.core.models import Group, PropertyMapping, User
 | 
					from authentik.core.models import Group, PropertyMapping, User
 | 
				
			||||||
from authentik.events.utils import sanitize_item
 | 
					from authentik.events.utils import sanitize_item
 | 
				
			||||||
 | 
					from authentik.lib.utils.errors import exception_to_string
 | 
				
			||||||
from authentik.policies.api.exec import PolicyTestSerializer
 | 
					from authentik.policies.api.exec import PolicyTestSerializer
 | 
				
			||||||
from authentik.rbac.decorators import permission_required
 | 
					from authentik.rbac.decorators import permission_required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -162,12 +164,15 @@ class PropertyMappingViewSet(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        response_data = {"successful": True, "result": ""}
 | 
					        response_data = {"successful": True, "result": ""}
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            result = mapping.evaluate(**context)
 | 
					            result = mapping.evaluate(dry_run=True, **context)
 | 
				
			||||||
            response_data["result"] = dumps(
 | 
					            response_data["result"] = dumps(
 | 
				
			||||||
                sanitize_item(result), indent=(4 if format_result else None)
 | 
					                sanitize_item(result), indent=(4 if format_result else None)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        except PropertyMappingExpressionException as exc:
 | 
				
			||||||
 | 
					            response_data["result"] = exception_to_string(exc.exc)
 | 
				
			||||||
 | 
					            response_data["successful"] = False
 | 
				
			||||||
        except Exception as exc:
 | 
					        except Exception as exc:
 | 
				
			||||||
            response_data["result"] = str(exc)
 | 
					            response_data["result"] = exception_to_string(exc)
 | 
				
			||||||
            response_data["successful"] = False
 | 
					            response_data["successful"] = False
 | 
				
			||||||
        response = PropertyMappingTestResultSerializer(response_data)
 | 
					        response = PropertyMappingTestResultSerializer(response_data)
 | 
				
			||||||
        return Response(response.data)
 | 
					        return Response(response.data)
 | 
				
			||||||
 | 
				
			|||||||
@ -678,10 +678,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        if not request.tenant.impersonation:
 | 
					        if not request.tenant.impersonation:
 | 
				
			||||||
            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)
 | 
				
			||||||
        if not request.user.has_perm("impersonate"):
 | 
					        user_to_be = self.get_object()
 | 
				
			||||||
 | 
					        if not request.user.has_perm("impersonate", user_to_be):
 | 
				
			||||||
            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)
 | 
				
			||||||
        user_to_be = self.get_object()
 | 
					 | 
				
			||||||
        if user_to_be.pk == self.request.user.pk:
 | 
					        if user_to_be.pk == self.request.user.pk:
 | 
				
			||||||
            LOGGER.debug("User attempted to impersonate themselves", user=request.user)
 | 
					            LOGGER.debug("User attempted to impersonate themselves", user=request.user)
 | 
				
			||||||
            return Response(status=401)
 | 
					            return Response(status=401)
 | 
				
			||||||
 | 
				
			|||||||
@ -9,10 +9,11 @@ class Command(TenantCommand):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def add_arguments(self, parser):
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
        parser.add_argument("--type", type=str, required=True)
 | 
					        parser.add_argument("--type", type=str, required=True)
 | 
				
			||||||
        parser.add_argument("--all", action="store_true")
 | 
					        parser.add_argument("--all", action="store_true", default=False)
 | 
				
			||||||
        parser.add_argument("usernames", nargs="+", type=str)
 | 
					        parser.add_argument("usernames", nargs="*", type=str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_per_tenant(self, **options):
 | 
					    def handle_per_tenant(self, **options):
 | 
				
			||||||
 | 
					        print(options)
 | 
				
			||||||
        new_type = UserTypes(options["type"])
 | 
					        new_type = UserTypes(options["type"])
 | 
				
			||||||
        qs = (
 | 
					        qs = (
 | 
				
			||||||
            User.objects.exclude_anonymous()
 | 
					            User.objects.exclude_anonymous()
 | 
				
			||||||
@ -22,6 +23,9 @@ class Command(TenantCommand):
 | 
				
			|||||||
        if options["usernames"] and options["all"]:
 | 
					        if options["usernames"] and options["all"]:
 | 
				
			||||||
            self.stderr.write("--all and usernames specified, only one can be specified")
 | 
					            self.stderr.write("--all and usernames specified, only one can be specified")
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					        if not options["usernames"] and not options["all"]:
 | 
				
			||||||
 | 
					            self.stderr.write("--all or usernames must be specified")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
        if options["usernames"] and not options["all"]:
 | 
					        if options["usernames"] and not options["all"]:
 | 
				
			||||||
            qs = qs.filter(username__in=options["usernames"])
 | 
					            qs = qs.filter(username__in=options["usernames"])
 | 
				
			||||||
        updated = qs.update(type=new_type)
 | 
					        updated = qs.update(type=new_type)
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ from model_utils.managers import InheritanceManager
 | 
				
			|||||||
from rest_framework.serializers import Serializer
 | 
					from rest_framework.serializers import Serializer
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.analytics.models import AnalyticsMixin
 | 
				
			||||||
from authentik.blueprints.models import ManagedModel
 | 
					from authentik.blueprints.models import ManagedModel
 | 
				
			||||||
from authentik.core.expression.exceptions import PropertyMappingExpressionException
 | 
					from authentik.core.expression.exceptions import PropertyMappingExpressionException
 | 
				
			||||||
from authentik.core.types import UILoginButton, UserSettingSerializer
 | 
					from authentik.core.types import UILoginButton, UserSettingSerializer
 | 
				
			||||||
@ -168,7 +169,7 @@ class GroupQuerySet(CTEQuerySet):
 | 
				
			|||||||
        return cte.join(Group, group_uuid=cte.col.group_uuid).with_cte(cte)
 | 
					        return cte.join(Group, group_uuid=cte.col.group_uuid).with_cte(cte)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Group(SerializerModel, AttributesMixin):
 | 
					class Group(SerializerModel, AttributesMixin, AnalyticsMixin):
 | 
				
			||||||
    """Group model which supports a basic hierarchy and has attributes"""
 | 
					    """Group model which supports a basic hierarchy and has attributes"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
					    group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
				
			||||||
@ -258,7 +259,7 @@ class UserManager(DjangoUserManager):
 | 
				
			|||||||
        return self.get_queryset().exclude_anonymous()
 | 
					        return self.get_queryset().exclude_anonymous()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
 | 
					class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser, AnalyticsMixin):
 | 
				
			||||||
    """authentik User model, based on django's contrib auth user model."""
 | 
					    """authentik User model, based on django's contrib auth user model."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
 | 
					    uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
 | 
				
			||||||
@ -376,7 +377,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
 | 
				
			|||||||
        return get_avatar(self)
 | 
					        return get_avatar(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Provider(SerializerModel):
 | 
					class Provider(SerializerModel, AnalyticsMixin):
 | 
				
			||||||
    """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
 | 
					    """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.TextField(unique=True)
 | 
					    name = models.TextField(unique=True)
 | 
				
			||||||
@ -466,13 +467,11 @@ class ApplicationQuerySet(QuerySet):
 | 
				
			|||||||
    def with_provider(self) -> "QuerySet[Application]":
 | 
					    def with_provider(self) -> "QuerySet[Application]":
 | 
				
			||||||
        qs = self.select_related("provider")
 | 
					        qs = self.select_related("provider")
 | 
				
			||||||
        for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
 | 
					        for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
 | 
				
			||||||
            if LOOKUP_SEP in subclass:
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            qs = qs.select_related(f"provider__{subclass}")
 | 
					            qs = qs.select_related(f"provider__{subclass}")
 | 
				
			||||||
        return qs
 | 
					        return qs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Application(SerializerModel, PolicyBindingModel):
 | 
					class Application(SerializerModel, PolicyBindingModel, AnalyticsMixin):
 | 
				
			||||||
    """Every Application which uses authentik for authentication/identification/authorization
 | 
					    """Every Application which uses authentik for authentication/identification/authorization
 | 
				
			||||||
    needs an Application record. Other authentication types can subclass this Model to
 | 
					    needs an Application record. Other authentication types can subclass this Model to
 | 
				
			||||||
    add custom fields and other properties"""
 | 
					    add custom fields and other properties"""
 | 
				
			||||||
@ -545,15 +544,24 @@ class Application(SerializerModel, PolicyBindingModel):
 | 
				
			|||||||
        if not self.provider:
 | 
					        if not self.provider:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
 | 
					        candidates = []
 | 
				
			||||||
            # We don't care about recursion, skip nested models
 | 
					        base_class = Provider
 | 
				
			||||||
            if LOOKUP_SEP in subclass:
 | 
					        for subclass in base_class.objects.get_queryset()._get_subclasses_recurse(base_class):
 | 
				
			||||||
 | 
					            parent = self.provider
 | 
				
			||||||
 | 
					            for level in subclass.split(LOOKUP_SEP):
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    parent = getattr(parent, level)
 | 
				
			||||||
 | 
					                except AttributeError:
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					            if parent in candidates:
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            try:
 | 
					            idx = subclass.count(LOOKUP_SEP)
 | 
				
			||||||
                return getattr(self.provider, subclass)
 | 
					            if type(parent) is not base_class:
 | 
				
			||||||
            except AttributeError:
 | 
					                idx += 1
 | 
				
			||||||
                pass
 | 
					            candidates.insert(idx, parent)
 | 
				
			||||||
        return None
 | 
					        if not candidates:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        return candidates[-1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return str(self.name)
 | 
					        return str(self.name)
 | 
				
			||||||
@ -596,7 +604,7 @@ class SourceGroupMatchingModes(models.TextChoices):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
 | 
					class Source(ManagedModel, SerializerModel, PolicyBindingModel, AnalyticsMixin):
 | 
				
			||||||
    """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
 | 
					    """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = models.TextField(help_text=_("Source's display Name."))
 | 
					    name = models.TextField(help_text=_("Source's display Name."))
 | 
				
			||||||
@ -728,7 +736,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
 | 
					class UserSourceConnection(SerializerModel, CreatedUpdatedModel, AnalyticsMixin):
 | 
				
			||||||
    """Connection between User and Source."""
 | 
					    """Connection between User and Source."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
					    user = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
				
			||||||
@ -748,7 +756,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
 | 
				
			|||||||
        unique_together = (("user", "source"),)
 | 
					        unique_together = (("user", "source"),)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
 | 
					class GroupSourceConnection(SerializerModel, CreatedUpdatedModel, AnalyticsMixin):
 | 
				
			||||||
    """Connection between Group and Source."""
 | 
					    """Connection between Group and Source."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
 | 
					    group = models.ForeignKey(Group, on_delete=models.CASCADE)
 | 
				
			||||||
@ -872,7 +880,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
 | 
				
			|||||||
        ).save()
 | 
					        ).save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PropertyMapping(SerializerModel, ManagedModel):
 | 
					class PropertyMapping(SerializerModel, ManagedModel, AnalyticsMixin):
 | 
				
			||||||
    """User-defined key -> x mapping which can be used by providers to expose extra data."""
 | 
					    """User-defined key -> x mapping which can be used by providers to expose extra data."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
					    pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
				
			||||||
@ -901,7 +909,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
 | 
				
			|||||||
        except ControlFlowException as exc:
 | 
					        except ControlFlowException as exc:
 | 
				
			||||||
            raise exc
 | 
					            raise exc
 | 
				
			||||||
        except Exception as exc:
 | 
					        except Exception as exc:
 | 
				
			||||||
            raise PropertyMappingExpressionException(self, exc) from exc
 | 
					            raise PropertyMappingExpressionException(exc, self) from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Property Mapping {self.name}"
 | 
					        return f"Property Mapping {self.name}"
 | 
				
			||||||
 | 
				
			|||||||
@ -9,9 +9,12 @@ from rest_framework.test import APITestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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.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
 | 
					from authentik.providers.oauth2.models import OAuth2Provider
 | 
				
			||||||
 | 
					from authentik.providers.proxy.models import ProxyProvider
 | 
				
			||||||
 | 
					from authentik.providers.saml.models import SAMLProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestApplicationsAPI(APITestCase):
 | 
					class TestApplicationsAPI(APITestCase):
 | 
				
			||||||
@ -222,3 +225,31 @@ class TestApplicationsAPI(APITestCase):
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_provider(self):
 | 
				
			||||||
 | 
					        """Ensure that proxy providers (at the time of writing that is the only provider
 | 
				
			||||||
 | 
					        that inherits from another proxy type (OAuth) instead of inheriting from the root
 | 
				
			||||||
 | 
					        provider class) is correctly looked up and selected from the database"""
 | 
				
			||||||
 | 
					        slug = generate_id()
 | 
				
			||||||
 | 
					        provider = ProxyProvider.objects.create(name=generate_id())
 | 
				
			||||||
 | 
					        Application.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            slug=slug,
 | 
				
			||||||
 | 
					            provider=provider,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(Application.objects.get(slug=slug).get_provider(), provider)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            Application.objects.with_provider().get(slug=slug).get_provider(), provider
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        slug = generate_id()
 | 
				
			||||||
 | 
					        provider = SAMLProvider.objects.create(name=generate_id())
 | 
				
			||||||
 | 
					        Application.objects.create(
 | 
				
			||||||
 | 
					            name=generate_id(),
 | 
				
			||||||
 | 
					            slug=slug,
 | 
				
			||||||
 | 
					            provider=provider,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(Application.objects.get(slug=slug).get_provider(), provider)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            Application.objects.with_provider().get(slug=slug).get_provider(), provider
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -3,10 +3,10 @@
 | 
				
			|||||||
from json import loads
 | 
					from json import loads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from guardian.shortcuts import assign_perm
 | 
				
			||||||
from rest_framework.test import APITestCase
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.tests.utils import create_test_admin_user, create_test_user
 | 
				
			||||||
from authentik.core.tests.utils import create_test_admin_user
 | 
					 | 
				
			||||||
from authentik.tenants.utils import get_current_tenant
 | 
					from authentik.tenants.utils import get_current_tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,7 +15,7 @@ class TestImpersonation(APITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def setUp(self) -> None:
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
        super().setUp()
 | 
					        super().setUp()
 | 
				
			||||||
        self.other_user = User.objects.create(username="to-impersonate")
 | 
					        self.other_user = create_test_user()
 | 
				
			||||||
        self.user = create_test_admin_user()
 | 
					        self.user = create_test_admin_user()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_impersonate_simple(self):
 | 
					    def test_impersonate_simple(self):
 | 
				
			||||||
@ -44,6 +44,26 @@ 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_scoped(self):
 | 
				
			||||||
 | 
					        """Test impersonation with scoped permissions"""
 | 
				
			||||||
 | 
					        new_user = create_test_user()
 | 
				
			||||||
 | 
					        assign_perm("authentik_core.impersonate", new_user, self.other_user)
 | 
				
			||||||
 | 
					        assign_perm("authentik_core.view_user", new_user, self.other_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_denied(self):
 | 
					    def test_impersonate_denied(self):
 | 
				
			||||||
        """test impersonation without permissions"""
 | 
					        """test impersonation without permissions"""
 | 
				
			||||||
        self.client.force_login(self.other_user)
 | 
					        self.client.force_login(self.other_user)
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ 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.core.models import User, UserTypes
 | 
					from authentik.core.models import User, UserTypes
 | 
				
			||||||
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
 | 
					from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
 | 
				
			||||||
from authentik.enterprise.models import License, LicenseUsageStatus
 | 
					from authentik.enterprise.models import License
 | 
				
			||||||
from authentik.rbac.decorators import permission_required
 | 
					from authentik.rbac.decorators import permission_required
 | 
				
			||||||
from authentik.tenants.utils import get_unique_identifier
 | 
					from authentik.tenants.utils import get_unique_identifier
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,7 +29,7 @@ class EnterpriseRequiredMixin:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def validate(self, attrs: dict) -> dict:
 | 
					    def validate(self, attrs: dict) -> dict:
 | 
				
			||||||
        """Check that a valid license exists"""
 | 
					        """Check that a valid license exists"""
 | 
				
			||||||
        if LicenseKey.cached_summary().status != LicenseUsageStatus.UNLICENSED:
 | 
					        if not LicenseKey.cached_summary().status.is_valid:
 | 
				
			||||||
            raise ValidationError(_("Enterprise is required to create/update this object."))
 | 
					            raise ValidationError(_("Enterprise is required to create/update this object."))
 | 
				
			||||||
        return super().validate(attrs)
 | 
					        return super().validate(attrs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -25,4 +25,4 @@ class AuthentikEnterpriseConfig(EnterpriseConfig):
 | 
				
			|||||||
        """Actual enterprise check, cached"""
 | 
					        """Actual enterprise check, cached"""
 | 
				
			||||||
        from authentik.enterprise.license import LicenseKey
 | 
					        from authentik.enterprise.license import LicenseKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return LicenseKey.cached_summary().status
 | 
					        return LicenseKey.cached_summary().status.is_valid
 | 
				
			||||||
 | 
				
			|||||||
@ -117,10 +117,13 @@ class LicenseKey:
 | 
				
			|||||||
                    our_cert.public_key(),
 | 
					                    our_cert.public_key(),
 | 
				
			||||||
                    algorithms=["ES512"],
 | 
					                    algorithms=["ES512"],
 | 
				
			||||||
                    audience=get_license_aud(),
 | 
					                    audience=get_license_aud(),
 | 
				
			||||||
                    options={"verify_exp": check_expiry},
 | 
					                    options={"verify_exp": check_expiry, "verify_signature": check_expiry},
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        except PyJWTError:
 | 
					        except PyJWTError:
 | 
				
			||||||
 | 
					            unverified = decode(jwt, options={"verify_signature": False})
 | 
				
			||||||
 | 
					            if unverified["aud"] != get_license_aud():
 | 
				
			||||||
 | 
					                raise ValidationError("Invalid Install ID in license") from None
 | 
				
			||||||
            raise ValidationError("Unable to verify license") from None
 | 
					            raise ValidationError("Unable to verify license") from None
 | 
				
			||||||
        return body
 | 
					        return body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -134,7 +137,7 @@ class LicenseKey:
 | 
				
			|||||||
            exp_ts = int(mktime(lic.expiry.timetuple()))
 | 
					            exp_ts = int(mktime(lic.expiry.timetuple()))
 | 
				
			||||||
            if total.exp == 0:
 | 
					            if total.exp == 0:
 | 
				
			||||||
                total.exp = exp_ts
 | 
					                total.exp = exp_ts
 | 
				
			||||||
            total.exp = min(total.exp, exp_ts)
 | 
					            total.exp = max(total.exp, exp_ts)
 | 
				
			||||||
            total.license_flags.extend(lic.status.license_flags)
 | 
					            total.license_flags.extend(lic.status.license_flags)
 | 
				
			||||||
        return total
 | 
					        return total
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.db.models.signals import post_save, pre_save
 | 
					from django.db.models.signals import post_delete, post_save, pre_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from django.utils.timezone import get_current_timezone
 | 
					from django.utils.timezone import get_current_timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,3 +27,9 @@ def post_save_license(sender: type[License], instance: License, **_):
 | 
				
			|||||||
    """Trigger license usage calculation when license is saved"""
 | 
					    """Trigger license usage calculation when license is saved"""
 | 
				
			||||||
    cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)
 | 
					    cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)
 | 
				
			||||||
    enterprise_update_usage.delay()
 | 
					    enterprise_update_usage.delay()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@receiver(post_delete, sender=License)
 | 
				
			||||||
 | 
					def post_delete_license(sender: type[License], instance: License, **_):
 | 
				
			||||||
 | 
					    """Clear license cache when license is deleted"""
 | 
				
			||||||
 | 
					    cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)
 | 
				
			||||||
 | 
				
			|||||||
@ -69,8 +69,5 @@ class NotificationViewSet(
 | 
				
			|||||||
    @action(detail=False, methods=["post"])
 | 
					    @action(detail=False, methods=["post"])
 | 
				
			||||||
    def mark_all_seen(self, request: Request) -> Response:
 | 
					    def mark_all_seen(self, request: Request) -> Response:
 | 
				
			||||||
        """Mark all the user's notifications as seen"""
 | 
					        """Mark all the user's notifications as seen"""
 | 
				
			||||||
        notifications = Notification.objects.filter(user=request.user)
 | 
					        Notification.objects.filter(user=request.user, seen=False).update(seen=True)
 | 
				
			||||||
        for notification in notifications:
 | 
					 | 
				
			||||||
            notification.seen = True
 | 
					 | 
				
			||||||
        Notification.objects.bulk_update(notifications, ["seen"])
 | 
					 | 
				
			||||||
        return Response({}, status=204)
 | 
					        return Response({}, status=204)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										49
									
								
								authentik/events/migrations/0008_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								authentik/events/migrations/0008_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.9 on 2024-09-25 11:06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                    ("secret_rotate", "Secret Rotate"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("flow_execution", "Flow Execution"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("system_exception", "System Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("email_sent", "Email Sent"),
 | 
				
			||||||
 | 
					                    ("analytics_sent", "Analytics Sent"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -49,6 +49,7 @@ from authentik.policies.models import PolicyBindingModel
 | 
				
			|||||||
from authentik.root.middleware import ClientIPMiddleware
 | 
					from authentik.root.middleware import ClientIPMiddleware
 | 
				
			||||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
					from authentik.stages.email.utils import TemplateEmailMessage
 | 
				
			||||||
from authentik.tenants.models import Tenant
 | 
					from authentik.tenants.models import Tenant
 | 
				
			||||||
 | 
					from authentik.tenants.utils import get_current_tenant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
DISCORD_FIELD_LIMIT = 25
 | 
					DISCORD_FIELD_LIMIT = 25
 | 
				
			||||||
@ -58,7 +59,11 @@ NOTIFICATION_SUMMARY_LENGTH = 75
 | 
				
			|||||||
def default_event_duration():
 | 
					def default_event_duration():
 | 
				
			||||||
    """Default duration an Event is saved.
 | 
					    """Default duration an Event is saved.
 | 
				
			||||||
    This is used as a fallback when no brand is available"""
 | 
					    This is used as a fallback when no brand is available"""
 | 
				
			||||||
    return now() + timedelta(days=365)
 | 
					    try:
 | 
				
			||||||
 | 
					        tenant = get_current_tenant()
 | 
				
			||||||
 | 
					        return now() + timedelta_from_string(tenant.event_retention)
 | 
				
			||||||
 | 
					    except Tenant.DoesNotExist:
 | 
				
			||||||
 | 
					        return now() + timedelta(days=365)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def default_brand():
 | 
					def default_brand():
 | 
				
			||||||
@ -114,6 +119,7 @@ class EventAction(models.TextChoices):
 | 
				
			|||||||
    MODEL_DELETED = "model_deleted"
 | 
					    MODEL_DELETED = "model_deleted"
 | 
				
			||||||
    EMAIL_SENT = "email_sent"
 | 
					    EMAIL_SENT = "email_sent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ANALYTICS_SENT = "analytics_sent"
 | 
				
			||||||
    UPDATE_AVAILABLE = "update_available"
 | 
					    UPDATE_AVAILABLE = "update_available"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    CUSTOM_PREFIX = "custom_"
 | 
					    CUSTOM_PREFIX = "custom_"
 | 
				
			||||||
@ -245,12 +251,6 @@ class Event(SerializerModel, ExpiringModel):
 | 
				
			|||||||
            if QS_QUERY in self.context["http_request"]["args"]:
 | 
					            if QS_QUERY in self.context["http_request"]["args"]:
 | 
				
			||||||
                wrapped = self.context["http_request"]["args"][QS_QUERY]
 | 
					                wrapped = self.context["http_request"]["args"][QS_QUERY]
 | 
				
			||||||
                self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped))
 | 
					                self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped))
 | 
				
			||||||
        if hasattr(request, "tenant"):
 | 
					 | 
				
			||||||
            tenant: Tenant = request.tenant
 | 
					 | 
				
			||||||
            # Because self.created only gets set on save, we can't use it's value here
 | 
					 | 
				
			||||||
            # hence we set self.created to now and then use it
 | 
					 | 
				
			||||||
            self.created = now()
 | 
					 | 
				
			||||||
            self.expires = self.created + timedelta_from_string(tenant.event_retention)
 | 
					 | 
				
			||||||
        if hasattr(request, "brand"):
 | 
					        if hasattr(request, "brand"):
 | 
				
			||||||
            brand: Brand = request.brand
 | 
					            brand: Brand = request.brand
 | 
				
			||||||
            self.brand = sanitize_dict(model_to_dict(brand))
 | 
					            self.brand = sanitize_dict(model_to_dict(brand))
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ 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
 | 
				
			||||||
@ -38,6 +38,9 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ from django.db.models import Model
 | 
				
			|||||||
from django.test import TestCase
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import default_token_key
 | 
					from authentik.core.models import default_token_key
 | 
				
			||||||
 | 
					from authentik.events.models import default_event_duration
 | 
				
			||||||
from authentik.lib.utils.reflection import get_apps
 | 
					from authentik.lib.utils.reflection import get_apps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,7 +21,7 @@ def model_tester_factory(test_model: type[Model]) -> Callable:
 | 
				
			|||||||
        allowed = 0
 | 
					        allowed = 0
 | 
				
			||||||
        # Token-like objects need to lookup the current tenant to get the default token length
 | 
					        # Token-like objects need to lookup the current tenant to get the default token length
 | 
				
			||||||
        for field in test_model._meta.fields:
 | 
					        for field in test_model._meta.fields:
 | 
				
			||||||
            if field.default == default_token_key:
 | 
					            if field.default in [default_token_key, default_event_duration]:
 | 
				
			||||||
                allowed += 1
 | 
					                allowed += 1
 | 
				
			||||||
        with self.assertNumQueries(allowed):
 | 
					        with self.assertNumQueries(allowed):
 | 
				
			||||||
            str(test_model())
 | 
					            str(test_model())
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from unittest.mock import MagicMock, patch
 | 
					from unittest.mock import MagicMock, patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import Group, User
 | 
					from authentik.core.models import Group, User
 | 
				
			||||||
from authentik.events.models import (
 | 
					from authentik.events.models import (
 | 
				
			||||||
@ -10,6 +11,7 @@ from authentik.events.models import (
 | 
				
			|||||||
    EventAction,
 | 
					    EventAction,
 | 
				
			||||||
    Notification,
 | 
					    Notification,
 | 
				
			||||||
    NotificationRule,
 | 
					    NotificationRule,
 | 
				
			||||||
 | 
					    NotificationSeverity,
 | 
				
			||||||
    NotificationTransport,
 | 
					    NotificationTransport,
 | 
				
			||||||
    NotificationWebhookMapping,
 | 
					    NotificationWebhookMapping,
 | 
				
			||||||
    TransportMode,
 | 
					    TransportMode,
 | 
				
			||||||
@ -20,7 +22,7 @@ from authentik.policies.exceptions import PolicyException
 | 
				
			|||||||
from authentik.policies.models import PolicyBinding
 | 
					from authentik.policies.models import PolicyBinding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestEventsNotifications(TestCase):
 | 
					class TestEventsNotifications(APITestCase):
 | 
				
			||||||
    """Test Event Notifications"""
 | 
					    """Test Event Notifications"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self) -> None:
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
@ -131,3 +133,15 @@ class TestEventsNotifications(TestCase):
 | 
				
			|||||||
        Notification.objects.all().delete()
 | 
					        Notification.objects.all().delete()
 | 
				
			||||||
        Event.new(EventAction.CUSTOM_PREFIX).save()
 | 
					        Event.new(EventAction.CUSTOM_PREFIX).save()
 | 
				
			||||||
        self.assertEqual(Notification.objects.first().body, "foo")
 | 
					        self.assertEqual(Notification.objects.first().body, "foo")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_api_mark_all_seen(self):
 | 
				
			||||||
 | 
					        """Test mark_all_seen"""
 | 
				
			||||||
 | 
					        self.client.force_login(self.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Notification.objects.create(
 | 
				
			||||||
 | 
					            severity=NotificationSeverity.NOTICE, body="foo", user=self.user, seen=False
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.post(reverse("authentik_api:notification-mark-all-seen"))
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 204)
 | 
				
			||||||
 | 
					        self.assertFalse(Notification.objects.filter(body="foo", seen=False).exists())
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
					        outpost_user = ClientIPMiddleware.get_outpost_user(request)
 | 
				
			||||||
        if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
 | 
					        if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
 | 
				
			||||||
            outpost_user = ClientIPMiddleware.get_outpost_user(request)
 | 
					 | 
				
			||||||
            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:
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import socket
 | 
					import socket
 | 
				
			||||||
from collections.abc import Iterable
 | 
					 | 
				
			||||||
from ipaddress import ip_address, ip_network
 | 
					from ipaddress import ip_address, ip_network
 | 
				
			||||||
from textwrap import indent
 | 
					from textwrap import indent
 | 
				
			||||||
from types import CodeType
 | 
					from types import CodeType
 | 
				
			||||||
@ -28,6 +27,12 @@ from authentik.stages.authenticator import devices_for_user
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARG_SANITIZE = re.compile(r"[:.-]")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def sanitize_arg(arg_name: str) -> str:
 | 
				
			||||||
 | 
					    return re.sub(ARG_SANITIZE, "_", arg_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BaseEvaluator:
 | 
					class BaseEvaluator:
 | 
				
			||||||
    """Validate and evaluate python-based expressions"""
 | 
					    """Validate and evaluate python-based expressions"""
 | 
				
			||||||
@ -177,9 +182,9 @@ class BaseEvaluator:
 | 
				
			|||||||
        proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
 | 
					        proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
 | 
				
			||||||
        return proc.profiling_wrapper()
 | 
					        return proc.profiling_wrapper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
 | 
					    def wrap_expression(self, expression: str) -> str:
 | 
				
			||||||
        """Wrap expression in a function, call it, and save the result as `result`"""
 | 
					        """Wrap expression in a function, call it, and save the result as `result`"""
 | 
				
			||||||
        handler_signature = ",".join(params)
 | 
					        handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
 | 
				
			||||||
        full_expression = ""
 | 
					        full_expression = ""
 | 
				
			||||||
        full_expression += f"def handler({handler_signature}):\n"
 | 
					        full_expression += f"def handler({handler_signature}):\n"
 | 
				
			||||||
        full_expression += indent(expression, "    ")
 | 
					        full_expression += indent(expression, "    ")
 | 
				
			||||||
@ -188,8 +193,8 @@ class BaseEvaluator:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def compile(self, expression: str) -> CodeType:
 | 
					    def compile(self, expression: str) -> CodeType:
 | 
				
			||||||
        """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
 | 
					        """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
 | 
				
			||||||
        param_keys = self._context.keys()
 | 
					        expression = self.wrap_expression(expression)
 | 
				
			||||||
        return compile(self.wrap_expression(expression, param_keys), self._filename, "exec")
 | 
					        return compile(expression, self._filename, "exec")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def evaluate(self, expression_source: str) -> Any:
 | 
					    def evaluate(self, expression_source: str) -> Any:
 | 
				
			||||||
        """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
 | 
					        """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
 | 
				
			||||||
@ -205,7 +210,7 @@ class BaseEvaluator:
 | 
				
			|||||||
                self.handle_error(exc, expression_source)
 | 
					                self.handle_error(exc, expression_source)
 | 
				
			||||||
                raise exc
 | 
					                raise exc
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                _locals = self._context
 | 
					                _locals = {sanitize_arg(x): y for x, y in self._context.items()}
 | 
				
			||||||
                # Yes this is an exec, yes it is potentially bad. Since we limit what variables are
 | 
					                # Yes this is an exec, yes it is potentially bad. Since we limit what variables are
 | 
				
			||||||
                # available here, and these policies can only be edited by admins, this is a risk
 | 
					                # available here, and these policies can only be edited by admins, this is a risk
 | 
				
			||||||
                # we're willing to take.
 | 
					                # we're willing to take.
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.9 on 2024-09-25 11:06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0023_alter_eventmatcherpolicy_action_and_more"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="eventmatcherpolicy",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                    ("secret_rotate", "Secret Rotate"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("flow_execution", "Flow Execution"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("system_exception", "System Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("email_sent", "Email Sent"),
 | 
				
			||||||
 | 
					                    ("analytics_sent", "Analytics Sent"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Match created events with this action type. When left empty, all action types will be matched.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -4,13 +4,13 @@ from django.apps.registry import Apps
 | 
				
			|||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations
 | 
					from django.db import migrations
 | 
				
			||||||
from django.contrib.auth.management import create_permissions
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
					def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
    from guardian.shortcuts import assign_perm
 | 
					 | 
				
			||||||
    from authentik.core.models import User
 | 
					    from authentik.core.models import User
 | 
				
			||||||
    from django.apps import apps as real_apps
 | 
					    from django.apps import apps as real_apps
 | 
				
			||||||
 | 
					    from django.contrib.auth.management import create_permissions
 | 
				
			||||||
 | 
					    from guardian.shortcuts import UserObjectPermission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db_alias = schema_editor.connection.alias
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,14 +20,25 @@ def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			|||||||
    create_permissions(real_apps.get_app_config("authentik_providers_ldap"), using=db_alias)
 | 
					    create_permissions(real_apps.get_app_config("authentik_providers_ldap"), using=db_alias)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider")
 | 
					    LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider")
 | 
				
			||||||
 | 
					    Permission = apps.get_model("auth", "Permission")
 | 
				
			||||||
 | 
					    UserObjectPermission = apps.get_model("guardian", "UserObjectPermission")
 | 
				
			||||||
 | 
					    ContentType = apps.get_model("contenttypes", "ContentType")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    new_prem = Permission.objects.using(db_alias).get(codename="search_full_directory")
 | 
				
			||||||
 | 
					    ct = ContentType.objects.using(db_alias).get(
 | 
				
			||||||
 | 
					        app_label="authentik_providers_ldap",
 | 
				
			||||||
 | 
					        model="ldapprovider",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for provider in LDAPProvider.objects.using(db_alias).all():
 | 
					    for provider in LDAPProvider.objects.using(db_alias).all():
 | 
				
			||||||
        for user_pk in (
 | 
					        if not provider.search_group:
 | 
				
			||||||
            provider.search_group.users.using(db_alias).all().values_list("pk", flat=True)
 | 
					            continue
 | 
				
			||||||
        ):
 | 
					        for user in provider.search_group.users.using(db_alias).all():
 | 
				
			||||||
            # We need the correct user model instance to assign the permission
 | 
					            UserObjectPermission.objects.using(db_alias).create(
 | 
				
			||||||
            assign_perm(
 | 
					                user=user,
 | 
				
			||||||
                "search_full_directory", User.objects.using(db_alias).get(pk=user_pk), provider
 | 
					                permission=new_prem,
 | 
				
			||||||
 | 
					                object_pk=provider.pk,
 | 
				
			||||||
 | 
					                content_type=ct,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,6 +46,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("authentik_providers_ldap", "0003_ldapprovider_mfa_support_and_more"),
 | 
					        ("authentik_providers_ldap", "0003_ldapprovider_mfa_support_and_more"),
 | 
				
			||||||
 | 
					        ("guardian", "0002_generic_permissions_index"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
 | 
				
			|||||||
@ -433,20 +433,21 @@ class TokenParams:
 | 
				
			|||||||
        app = Application.objects.filter(provider=self.provider).first()
 | 
					        app = Application.objects.filter(provider=self.provider).first()
 | 
				
			||||||
        if not app or not app.provider:
 | 
					        if not app or not app.provider:
 | 
				
			||||||
            raise TokenError("invalid_grant")
 | 
					            raise TokenError("invalid_grant")
 | 
				
			||||||
        self.user, _ = User.objects.update_or_create(
 | 
					        with audit_ignore():
 | 
				
			||||||
            # trim username to ensure the entire username is max 150 chars
 | 
					            self.user, _ = User.objects.update_or_create(
 | 
				
			||||||
            # (22 chars being the length of the "template")
 | 
					                # trim username to ensure the entire username is max 150 chars
 | 
				
			||||||
            username=f"ak-{self.provider.name[:150-22]}-client_credentials",
 | 
					                # (22 chars being the length of the "template")
 | 
				
			||||||
            defaults={
 | 
					                username=f"ak-{self.provider.name[:150-22]}-client_credentials",
 | 
				
			||||||
                "attributes": {
 | 
					                defaults={
 | 
				
			||||||
                    USER_ATTRIBUTE_GENERATED: True,
 | 
					                    "attributes": {
 | 
				
			||||||
 | 
					                        USER_ATTRIBUTE_GENERATED: True,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    "last_login": timezone.now(),
 | 
				
			||||||
 | 
					                    "name": f"Autogenerated user from application {app.name} (client credentials)",
 | 
				
			||||||
 | 
					                    "path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
 | 
				
			||||||
 | 
					                    "type": UserTypes.SERVICE_ACCOUNT,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                "last_login": timezone.now(),
 | 
					            )
 | 
				
			||||||
                "name": f"Autogenerated user from application {app.name} (client credentials)",
 | 
					 | 
				
			||||||
                "path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
 | 
					 | 
				
			||||||
                "type": UserTypes.SERVICE_ACCOUNT,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        self.__check_policy_access(app, request)
 | 
					        self.__check_policy_access(app, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Event.new(
 | 
					        Event.new(
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,7 @@ class ProxyDockerController(DockerController):
 | 
				
			|||||||
        labels = super()._get_labels()
 | 
					        labels = super()._get_labels()
 | 
				
			||||||
        labels["traefik.enable"] = "true"
 | 
					        labels["traefik.enable"] = "true"
 | 
				
			||||||
        labels[f"traefik.http.routers.{traefik_name}-router.rule"] = (
 | 
					        labels[f"traefik.http.routers.{traefik_name}-router.rule"] = (
 | 
				
			||||||
            f"({' || '.join([f'Host(`{host}`)' for host in hosts])})"
 | 
					            f"({' || '.join([f'Host({host})' for host in hosts])})"
 | 
				
			||||||
            f" && PathPrefix(`/outpost.goauthentik.io`)"
 | 
					            f" && PathPrefix(`/outpost.goauthentik.io`)"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
 | 
					        labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
 | 
				
			||||||
 | 
				
			|||||||
@ -164,7 +164,7 @@ class SAMLProvider(Provider):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sign_assertion = models.BooleanField(default=True)
 | 
					    sign_assertion = models.BooleanField(default=True)
 | 
				
			||||||
    sign_response = models.BooleanField(default=True)
 | 
					    sign_response = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def launch_url(self) -> str | None:
 | 
					    def launch_url(self) -> str | None:
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,11 @@ class TestServiceProviderMetadataParser(TestCase):
 | 
				
			|||||||
        request = self.factory.get("/")
 | 
					        request = self.factory.get("/")
 | 
				
			||||||
        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 | 
					        metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd"))  # nosec
 | 
					        schema = etree.XMLSchema(
 | 
				
			||||||
 | 
					            etree.parse(
 | 
				
			||||||
 | 
					                source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
 | 
				
			||||||
 | 
					            )  # nosec
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertTrue(schema.validate(metadata))
 | 
					        self.assertTrue(schema.validate(metadata))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_schema_want_authn_requests_signed(self):
 | 
					    def test_schema_want_authn_requests_signed(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -47,7 +47,9 @@ class TestSchema(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        metadata = lxml_from_string(request)
 | 
					        metadata = lxml_from_string(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd"))  # nosec
 | 
					        schema = etree.XMLSchema(
 | 
				
			||||||
 | 
					            etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser())  # nosec
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertTrue(schema.validate(metadata))
 | 
					        self.assertTrue(schema.validate(metadata))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_response_schema(self):
 | 
					    def test_response_schema(self):
 | 
				
			||||||
@ -68,5 +70,7 @@ class TestSchema(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        metadata = lxml_from_string(response)
 | 
					        metadata = lxml_from_string(response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd"))  # nosec
 | 
					        schema = etree.XMLSchema(
 | 
				
			||||||
 | 
					            etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser())  # nosec
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertTrue(schema.validate(metadata))
 | 
					        self.assertTrue(schema.validate(metadata))
 | 
				
			||||||
 | 
				
			|||||||
@ -87,7 +87,11 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def _get_startup_tasks_default_tenant() -> list[Callable]:
 | 
					def _get_startup_tasks_default_tenant() -> list[Callable]:
 | 
				
			||||||
    """Get all tasks to be run on startup for the default tenant"""
 | 
					    """Get all tasks to be run on startup for the default tenant"""
 | 
				
			||||||
    return []
 | 
					    from authentik.outposts.tasks import outpost_connection_discovery
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        outpost_connection_discovery,
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _get_startup_tasks_all_tenants() -> list[Callable]:
 | 
					def _get_startup_tasks_all_tenants() -> list[Callable]:
 | 
				
			||||||
 | 
				
			|||||||
@ -221,9 +221,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)
 | 
				
			||||||
        return delegated_ip
 | 
					        return delegated_ip
 | 
				
			||||||
 | 
				
			|||||||
@ -70,6 +70,7 @@ TENANT_APPS = [
 | 
				
			|||||||
    "django.contrib.contenttypes",
 | 
					    "django.contrib.contenttypes",
 | 
				
			||||||
    "django.contrib.sessions",
 | 
					    "django.contrib.sessions",
 | 
				
			||||||
    "authentik.admin",
 | 
					    "authentik.admin",
 | 
				
			||||||
 | 
					    "authentik.analytics",
 | 
				
			||||||
    "authentik.api",
 | 
					    "authentik.api",
 | 
				
			||||||
    "authentik.crypto",
 | 
					    "authentik.crypto",
 | 
				
			||||||
    "authentik.flows",
 | 
					    "authentik.flows",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
"""authentik storage backends"""
 | 
					"""authentik storage backends"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					from urllib.parse import parse_qsl, urlsplit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core.exceptions import SuspiciousOperation
 | 
					from django.core.exceptions import SuspiciousOperation
 | 
				
			||||||
@ -110,3 +111,34 @@ class S3Storage(BaseS3Storage):
 | 
				
			|||||||
        if self.querystring_auth:
 | 
					        if self.querystring_auth:
 | 
				
			||||||
            return url
 | 
					            return url
 | 
				
			||||||
        return self._strip_signing_parameters(url)
 | 
					        return self._strip_signing_parameters(url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _strip_signing_parameters(self, url):
 | 
				
			||||||
 | 
					        # Boto3 does not currently support generating URLs that are unsigned. Instead
 | 
				
			||||||
 | 
					        # we take the signed URLs and strip any querystring params related to signing
 | 
				
			||||||
 | 
					        # and expiration.
 | 
				
			||||||
 | 
					        # Note that this may end up with URLs that are still invalid, especially if
 | 
				
			||||||
 | 
					        # params are passed in that only work with signed URLs, e.g. response header
 | 
				
			||||||
 | 
					        # params.
 | 
				
			||||||
 | 
					        # The code attempts to strip all query parameters that match names of known
 | 
				
			||||||
 | 
					        # parameters from v2 and v4 signatures, regardless of the actual signature
 | 
				
			||||||
 | 
					        # version used.
 | 
				
			||||||
 | 
					        split_url = urlsplit(url)
 | 
				
			||||||
 | 
					        qs = parse_qsl(split_url.query, keep_blank_values=True)
 | 
				
			||||||
 | 
					        blacklist = {
 | 
				
			||||||
 | 
					            "x-amz-algorithm",
 | 
				
			||||||
 | 
					            "x-amz-credential",
 | 
				
			||||||
 | 
					            "x-amz-date",
 | 
				
			||||||
 | 
					            "x-amz-expires",
 | 
				
			||||||
 | 
					            "x-amz-signedheaders",
 | 
				
			||||||
 | 
					            "x-amz-signature",
 | 
				
			||||||
 | 
					            "x-amz-security-token",
 | 
				
			||||||
 | 
					            "awsaccesskeyid",
 | 
				
			||||||
 | 
					            "expires",
 | 
				
			||||||
 | 
					            "signature",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        filtered_qs = ((key, val) for key, val in qs if key.lower() not in blacklist)
 | 
				
			||||||
 | 
					        # Note: Parameters that did not have a value in the original query string will
 | 
				
			||||||
 | 
					        # have an '=' sign appended to it, e.g ?foo&bar becomes ?foo=&bar=
 | 
				
			||||||
 | 
					        joined_qs = ("=".join(keyval) for keyval in filtered_qs)
 | 
				
			||||||
 | 
					        split_url = split_url._replace(query="&".join(joined_qs))
 | 
				
			||||||
 | 
					        return split_url.geturl()
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,11 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			|||||||
            search_base=self.base_dn_groups,
 | 
					            search_base=self.base_dn_groups,
 | 
				
			||||||
            search_filter=self._source.group_object_filter,
 | 
					            search_filter=self._source.group_object_filter,
 | 
				
			||||||
            search_scope=SUBTREE,
 | 
					            search_scope=SUBTREE,
 | 
				
			||||||
            attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
 | 
					            attributes=[
 | 
				
			||||||
 | 
					                ALL_ATTRIBUTES,
 | 
				
			||||||
 | 
					                ALL_OPERATIONAL_ATTRIBUTES,
 | 
				
			||||||
 | 
					                self._source.object_uniqueness_field,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
            **kwargs,
 | 
					            **kwargs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,9 +57,9 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			|||||||
                continue
 | 
					                continue
 | 
				
			||||||
            attributes = group.get("attributes", {})
 | 
					            attributes = group.get("attributes", {})
 | 
				
			||||||
            group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
 | 
					            group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
 | 
				
			||||||
            if self._source.object_uniqueness_field not in attributes:
 | 
					            if not attributes.get(self._source.object_uniqueness_field):
 | 
				
			||||||
                self.message(
 | 
					                self.message(
 | 
				
			||||||
                    f"Cannot find uniqueness field in attributes: '{group_dn}'",
 | 
					                    f"Uniqueness field not found/not set in attributes: '{group_dn}'",
 | 
				
			||||||
                    attributes=attributes.keys(),
 | 
					                    attributes=attributes.keys(),
 | 
				
			||||||
                    dn=group_dn,
 | 
					                    dn=group_dn,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,11 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			|||||||
            search_base=self.base_dn_users,
 | 
					            search_base=self.base_dn_users,
 | 
				
			||||||
            search_filter=self._source.user_object_filter,
 | 
					            search_filter=self._source.user_object_filter,
 | 
				
			||||||
            search_scope=SUBTREE,
 | 
					            search_scope=SUBTREE,
 | 
				
			||||||
            attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
 | 
					            attributes=[
 | 
				
			||||||
 | 
					                ALL_ATTRIBUTES,
 | 
				
			||||||
 | 
					                ALL_OPERATIONAL_ATTRIBUTES,
 | 
				
			||||||
 | 
					                self._source.object_uniqueness_field,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
            **kwargs,
 | 
					            **kwargs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,9 +59,9 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			|||||||
                continue
 | 
					                continue
 | 
				
			||||||
            attributes = user.get("attributes", {})
 | 
					            attributes = user.get("attributes", {})
 | 
				
			||||||
            user_dn = flatten(user.get("entryDN", user.get("dn")))
 | 
					            user_dn = flatten(user.get("entryDN", user.get("dn")))
 | 
				
			||||||
            if self._source.object_uniqueness_field not in attributes:
 | 
					            if not attributes.get(self._source.object_uniqueness_field):
 | 
				
			||||||
                self.message(
 | 
					                self.message(
 | 
				
			||||||
                    f"Cannot find uniqueness field in attributes: '{user_dn}'",
 | 
					                    f"Uniqueness field not found/not set in attributes: '{user_dn}'",
 | 
				
			||||||
                    attributes=attributes.keys(),
 | 
					                    attributes=attributes.keys(),
 | 
				
			||||||
                    dn=user_dn,
 | 
					                    dn=user_dn,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,9 @@ class TestMetadataProcessor(TestCase):
 | 
				
			|||||||
        xml = MetadataProcessor(self.source, request).build_entity_descriptor()
 | 
					        xml = MetadataProcessor(self.source, request).build_entity_descriptor()
 | 
				
			||||||
        metadata = lxml_from_string(xml)
 | 
					        metadata = lxml_from_string(xml)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd"))  # nosec
 | 
					        schema = etree.XMLSchema(
 | 
				
			||||||
 | 
					            etree.parse("schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser())  # nosec
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.assertTrue(schema.validate(metadata))
 | 
					        self.assertTrue(schema.validate(metadata))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_metadata_consistent(self):
 | 
					    def test_metadata_consistent(self):
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,9 +1,12 @@
 | 
				
			|||||||
"""Serializer for tenants models"""
 | 
					"""Serializer for tenants models"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django_tenants.utils import get_public_schema_name
 | 
					from django_tenants.utils import get_public_schema_name
 | 
				
			||||||
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
from rest_framework.generics import RetrieveUpdateAPIView
 | 
					from rest_framework.generics import RetrieveUpdateAPIView
 | 
				
			||||||
from rest_framework.permissions import SAFE_METHODS
 | 
					from rest_framework.permissions import SAFE_METHODS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.analytics.api import AnalyticsDescriptionSerializer
 | 
				
			||||||
 | 
					from authentik.analytics.utils import get_analytics_description
 | 
				
			||||||
from authentik.core.api.utils import ModelSerializer
 | 
					from authentik.core.api.utils import ModelSerializer
 | 
				
			||||||
from authentik.rbac.permissions import HasPermission
 | 
					from authentik.rbac.permissions import HasPermission
 | 
				
			||||||
from authentik.tenants.models import Tenant
 | 
					from authentik.tenants.models import Tenant
 | 
				
			||||||
@ -12,6 +15,8 @@ from authentik.tenants.models import Tenant
 | 
				
			|||||||
class SettingsSerializer(ModelSerializer):
 | 
					class SettingsSerializer(ModelSerializer):
 | 
				
			||||||
    """Settings Serializer"""
 | 
					    """Settings Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    analytics_sources_obj = SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Tenant
 | 
					        model = Tenant
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
@ -25,8 +30,19 @@ class SettingsSerializer(ModelSerializer):
 | 
				
			|||||||
            "impersonation",
 | 
					            "impersonation",
 | 
				
			||||||
            "default_token_duration",
 | 
					            "default_token_duration",
 | 
				
			||||||
            "default_token_length",
 | 
					            "default_token_length",
 | 
				
			||||||
 | 
					            "default_token_length",
 | 
				
			||||||
 | 
					            "analytics_enabled",
 | 
				
			||||||
 | 
					            "analytics_sources",
 | 
				
			||||||
 | 
					            "analytics_sources_obj",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_analytics_sources_obj(self, obj: Tenant) -> list[AnalyticsDescriptionSerializer]:
 | 
				
			||||||
 | 
					        result = []
 | 
				
			||||||
 | 
					        for label, desc in get_analytics_description().items():
 | 
				
			||||||
 | 
					            if label in obj.analytics_sources:
 | 
				
			||||||
 | 
					                result.append((label, desc))
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SettingsView(RetrieveUpdateAPIView):
 | 
					class SettingsView(RetrieveUpdateAPIView):
 | 
				
			||||||
    """Settings view"""
 | 
					    """Settings view"""
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.0.9 on 2024-09-24 15:36
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.contrib.postgres.fields
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_tenants", "0003_alter_tenant_default_token_duration"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="tenant",
 | 
				
			||||||
 | 
					            name="analytics_enabled",
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="tenant",
 | 
				
			||||||
 | 
					            name="analytics_sources",
 | 
				
			||||||
 | 
					            field=django.contrib.postgres.fields.ArrayField(
 | 
				
			||||||
 | 
					                base_field=models.TextField(), blank=True, default=list, size=None
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -4,6 +4,7 @@ import re
 | 
				
			|||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import apps
 | 
					from django.apps import apps
 | 
				
			||||||
 | 
					from django.contrib.postgres.fields import ArrayField
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
from django.core.validators import MinValueValidator
 | 
					from django.core.validators import MinValueValidator
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
@ -96,6 +97,9 @@ class Tenant(TenantMixin, SerializerModel):
 | 
				
			|||||||
        validators=[MinValueValidator(1)],
 | 
					        validators=[MinValueValidator(1)],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    analytics_enabled = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    analytics_sources = ArrayField(models.TextField(), blank=True, default=list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        if self.schema_name == "template":
 | 
					        if self.schema_name == "template":
 | 
				
			||||||
            raise IntegrityError("Cannot create schema named template")
 | 
					            raise IntegrityError("Cannot create schema named template")
 | 
				
			||||||
 | 
				
			|||||||
@ -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.6.4 Blueprint schema",
 | 
					    "title": "authentik 2024.8.2 Blueprint schema",
 | 
				
			||||||
    "required": [
 | 
					    "required": [
 | 
				
			||||||
        "version",
 | 
					        "version",
 | 
				
			||||||
        "entries"
 | 
					        "entries"
 | 
				
			||||||
@ -4227,6 +4227,7 @@
 | 
				
			|||||||
                        "model_updated",
 | 
					                        "model_updated",
 | 
				
			||||||
                        "model_deleted",
 | 
					                        "model_deleted",
 | 
				
			||||||
                        "email_sent",
 | 
					                        "email_sent",
 | 
				
			||||||
 | 
					                        "analytics_sent",
 | 
				
			||||||
                        "update_available",
 | 
					                        "update_available",
 | 
				
			||||||
                        "custom_"
 | 
					                        "custom_"
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
@ -4251,6 +4252,7 @@
 | 
				
			|||||||
                        null,
 | 
					                        null,
 | 
				
			||||||
                        "authentik.tenants",
 | 
					                        "authentik.tenants",
 | 
				
			||||||
                        "authentik.admin",
 | 
					                        "authentik.admin",
 | 
				
			||||||
 | 
					                        "authentik.analytics",
 | 
				
			||||||
                        "authentik.api",
 | 
					                        "authentik.api",
 | 
				
			||||||
                        "authentik.crypto",
 | 
					                        "authentik.crypto",
 | 
				
			||||||
                        "authentik.flows",
 | 
					                        "authentik.flows",
 | 
				
			||||||
@ -13116,6 +13118,7 @@
 | 
				
			|||||||
                        "model_updated",
 | 
					                        "model_updated",
 | 
				
			||||||
                        "model_deleted",
 | 
					                        "model_deleted",
 | 
				
			||||||
                        "email_sent",
 | 
					                        "email_sent",
 | 
				
			||||||
 | 
					                        "analytics_sent",
 | 
				
			||||||
                        "update_available",
 | 
					                        "update_available",
 | 
				
			||||||
                        "custom_"
 | 
					                        "custom_"
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
@ -13277,6 +13280,7 @@
 | 
				
			|||||||
                                "model_updated",
 | 
					                                "model_updated",
 | 
				
			||||||
                                "model_deleted",
 | 
					                                "model_deleted",
 | 
				
			||||||
                                "email_sent",
 | 
					                                "email_sent",
 | 
				
			||||||
 | 
					                                "analytics_sent",
 | 
				
			||||||
                                "update_available",
 | 
					                                "update_available",
 | 
				
			||||||
                                "custom_"
 | 
					                                "custom_"
 | 
				
			||||||
                            ],
 | 
					                            ],
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ services:
 | 
				
			|||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - redis:/data
 | 
					      - redis:/data
 | 
				
			||||||
  server:
 | 
					  server:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.2}
 | 
				
			||||||
    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.6.4}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.2}
 | 
				
			||||||
    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.2024082.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.2024082.1 h1:V/3tq3rGK8Fse6xqnVQ8epzzytjXRI93y+jNHen2zMQ=
 | 
				
			||||||
goauthentik.io/api/v3 v3.2024064.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
 | 
					goauthentik.io/api/v3 v3.2024082.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.6.4"
 | 
					const VERSION = "2024.8.2"
 | 
				
			||||||
 | 
				
			|||||||
@ -35,10 +35,11 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
 | 
				
			|||||||
	req PaginatorRequest[Treq, Tres],
 | 
						req PaginatorRequest[Treq, Tres],
 | 
				
			||||||
	opts PaginatorOptions,
 | 
						opts PaginatorOptions,
 | 
				
			||||||
) ([]Tobj, error) {
 | 
					) ([]Tobj, error) {
 | 
				
			||||||
 | 
						var bfreq, cfreq interface{}
 | 
				
			||||||
	fetchOffset := func(page int32) (Tres, error) {
 | 
						fetchOffset := func(page int32) (Tres, error) {
 | 
				
			||||||
		req.Page(page)
 | 
							bfreq = req.Page(page)
 | 
				
			||||||
		req.PageSize(int32(opts.PageSize))
 | 
							cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
 | 
				
			||||||
		res, _, err := req.Execute()
 | 
							res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
 | 
								opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										26
									
								
								internal/outpost/ak/api_utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/outpost/ak/api_utils_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					package ak
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// func Test_PaginatorCompile(t *testing.T) {
 | 
				
			||||||
 | 
					// 	req := api.ApiCoreUsersListRequest{}
 | 
				
			||||||
 | 
					// 	Paginator(req, PaginatorOptions{
 | 
				
			||||||
 | 
					// 		PageSize: 100,
 | 
				
			||||||
 | 
					// 	})
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// func Test_PaginatorCompileExplicit(t *testing.T) {
 | 
				
			||||||
 | 
					// 	req := api.ApiCoreUsersListRequest{}
 | 
				
			||||||
 | 
					// 	Paginator[
 | 
				
			||||||
 | 
					// 		api.User,
 | 
				
			||||||
 | 
					// 		api.ApiCoreUsersListRequest,
 | 
				
			||||||
 | 
					// 		*api.PaginatedUserList,
 | 
				
			||||||
 | 
					// 	](req, PaginatorOptions{
 | 
				
			||||||
 | 
					// 		PageSize: 100,
 | 
				
			||||||
 | 
					// 	})
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// func Test_PaginatorCompileOther(t *testing.T) {
 | 
				
			||||||
 | 
					// 	req := api.ApiOutpostsProxyListRequest{}
 | 
				
			||||||
 | 
					// 	Paginator(req, PaginatorOptions{
 | 
				
			||||||
 | 
					// 		PageSize: 100,
 | 
				
			||||||
 | 
					// 	})
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
@ -96,7 +96,7 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
 | 
				
			|||||||
		return ldap.LDAPResultOperationsError, nil
 | 
							return ldap.LDAPResultOperationsError, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	flags.UserPk = userInfo.User.Pk
 | 
						flags.UserPk = userInfo.User.Pk
 | 
				
			||||||
	flags.CanSearch = access.HasSearchPermission != nil
 | 
						flags.CanSearch = access.GetHasSearchPermission()
 | 
				
			||||||
	db.si.SetFlags(req.BindDN, &flags)
 | 
						db.si.SetFlags(req.BindDN, &flags)
 | 
				
			||||||
	if flags.CanSearch {
 | 
						if flags.CanSearch {
 | 
				
			||||||
		req.Log().Debug("Allowed access to search")
 | 
							req.Log().Debug("Allowed access to search")
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
				
			||||||
@ -193,7 +204,17 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) {
 | 
						mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		a.handleAuthStart(w, r, "")
 | 
							fwd := ""
 | 
				
			||||||
 | 
							// This should only really be hit for nginx forward_auth
 | 
				
			||||||
 | 
							// as for that the auth start redirect URL is generated by the
 | 
				
			||||||
 | 
							// reverse proxy, and as such we won't have a request we just
 | 
				
			||||||
 | 
							// denied to reference for final URL
 | 
				
			||||||
 | 
							rd, ok := a.checkRedirectParam(r)
 | 
				
			||||||
 | 
							if ok {
 | 
				
			||||||
 | 
								a.log.WithField("rd", rd).Trace("Setting redirect")
 | 
				
			||||||
 | 
								fwd = rd
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							a.handleAuthStart(w, r, fwd)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback)
 | 
						mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback)
 | 
				
			||||||
	mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)
 | 
						mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)
 | 
				
			||||||
@ -225,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
 | 
				
			||||||
 | 
				
			|||||||
@ -15,36 +15,6 @@ const (
 | 
				
			|||||||
	LogoutSignature   = "X-authentik-logout"
 | 
						LogoutSignature   = "X-authentik-logout"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
 | 
					 | 
				
			||||||
	rd := r.URL.Query().Get(redirectParam)
 | 
					 | 
				
			||||||
	if rd == "" {
 | 
					 | 
				
			||||||
		return "", false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	u, err := url.Parse(rd)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		a.log.WithError(err).Warning("Failed to parse redirect URL")
 | 
					 | 
				
			||||||
		return "", false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// Check to make sure we only redirect to allowed places
 | 
					 | 
				
			||||||
	if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
 | 
					 | 
				
			||||||
		ext, err := url.Parse(a.proxyConfig.ExternalHost)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return "", false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		ext.Scheme = ""
 | 
					 | 
				
			||||||
		if !strings.Contains(u.String(), ext.String()) {
 | 
					 | 
				
			||||||
			a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host")
 | 
					 | 
				
			||||||
			return "", false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
 | 
					 | 
				
			||||||
			a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
 | 
					 | 
				
			||||||
			return "", false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return u.String(), true
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
 | 
					func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
 | 
				
			||||||
	state, err := a.createState(r, fwd)
 | 
						state, err := a.createState(r, fwd)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,13 @@ import (
 | 
				
			|||||||
	"encoding/base64"
 | 
						"encoding/base64"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/golang-jwt/jwt/v5"
 | 
						"github.com/golang-jwt/jwt/v5"
 | 
				
			||||||
	"github.com/gorilla/securecookie"
 | 
						"github.com/gorilla/securecookie"
 | 
				
			||||||
	"github.com/mitchellh/mapstructure"
 | 
						"github.com/mitchellh/mapstructure"
 | 
				
			||||||
 | 
						"goauthentik.io/api/v3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OAuthState struct {
 | 
					type OAuthState struct {
 | 
				
			||||||
@ -27,6 +30,44 @@ func (oas *OAuthState) GetAudience() (jwt.ClaimStrings, error)       { return ni
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
 | 
					var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Validate that the given redirect parameter (?rd=...) is valid and can be used
 | 
				
			||||||
 | 
					// For proxy/forward_single this checks that if the `rd` param has a Hostname (and is a full URL)
 | 
				
			||||||
 | 
					// the hostname matches what's configured, or no hostname must be given
 | 
				
			||||||
 | 
					// For forward_domain this checks if the domain of the URL in `rd` ends with the configured domain
 | 
				
			||||||
 | 
					func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
 | 
				
			||||||
 | 
						rd := r.URL.Query().Get(redirectParam)
 | 
				
			||||||
 | 
						if rd == "" {
 | 
				
			||||||
 | 
							return "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						u, err := url.Parse(rd)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							a.log.WithError(err).Warning("Failed to parse redirect URL")
 | 
				
			||||||
 | 
							return "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Check to make sure we only redirect to allowed places
 | 
				
			||||||
 | 
						if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
 | 
				
			||||||
 | 
							ext, err := url.Parse(a.proxyConfig.ExternalHost)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Either hostname needs to match the configured domain, or host name must be empty for just a path
 | 
				
			||||||
 | 
							if u.Host == "" {
 | 
				
			||||||
 | 
								u.Host = ext.Host
 | 
				
			||||||
 | 
								u.Scheme = ext.Scheme
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if u.Host != ext.Host {
 | 
				
			||||||
 | 
								a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host")
 | 
				
			||||||
 | 
								return "", false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
 | 
				
			||||||
 | 
								a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
 | 
				
			||||||
 | 
								return "", false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return u.String(), true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (a *Application) createState(r *http.Request, fwd string) (string, error) {
 | 
					func (a *Application) createState(r *http.Request, fwd string) (string, error) {
 | 
				
			||||||
	s, _ := a.sessions.Get(r, a.SessionName())
 | 
						s, _ := a.sessions.Get(r, a.SessionName())
 | 
				
			||||||
	if s.ID == "" {
 | 
						if s.ID == "" {
 | 
				
			||||||
@ -39,17 +80,6 @@ func (a *Application) createState(r *http.Request, fwd string) (string, error) {
 | 
				
			|||||||
		SessionID: s.ID,
 | 
							SessionID: s.ID,
 | 
				
			||||||
		Redirect:  fwd,
 | 
							Redirect:  fwd,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if fwd == "" {
 | 
					 | 
				
			||||||
		// This should only really be hit for nginx forward_auth
 | 
					 | 
				
			||||||
		// as for that the auth start redirect URL is generated by the
 | 
					 | 
				
			||||||
		// reverse proxy, and as such we won't have a request we just
 | 
					 | 
				
			||||||
		// denied to reference for final URL
 | 
					 | 
				
			||||||
		rd, ok := a.checkRedirectParam(r)
 | 
					 | 
				
			||||||
		if ok {
 | 
					 | 
				
			||||||
			a.log.WithField("rd", rd).Trace("Setting redirect")
 | 
					 | 
				
			||||||
			st.Redirect = rd
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, st)
 | 
						token := jwt.NewWithClaims(jwt.SigningMethodHS256, st)
 | 
				
			||||||
	tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret()))
 | 
						tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret()))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,25 +8,45 @@ import (
 | 
				
			|||||||
	"goauthentik.io/api/v3"
 | 
						"goauthentik.io/api/v3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCheckRedirectParam(t *testing.T) {
 | 
					func TestCheckRedirectParam_None(t *testing.T) {
 | 
				
			||||||
	a := newTestApplication()
 | 
						a := newTestApplication()
 | 
				
			||||||
 | 
						// Test no rd param
 | 
				
			||||||
	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil)
 | 
						req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rd, ok := a.checkRedirectParam(req)
 | 
						rd, ok := a.checkRedirectParam(req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.Equal(t, false, ok)
 | 
						assert.Equal(t, false, ok)
 | 
				
			||||||
	assert.Equal(t, "", rd)
 | 
						assert.Equal(t, "", rd)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil)
 | 
					func TestCheckRedirectParam_Invalid(t *testing.T) {
 | 
				
			||||||
 | 
						a := newTestApplication()
 | 
				
			||||||
 | 
						// Test invalid rd param
 | 
				
			||||||
 | 
						req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rd, ok = a.checkRedirectParam(req)
 | 
						rd, ok := a.checkRedirectParam(req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.Equal(t, false, ok)
 | 
						assert.Equal(t, false, ok)
 | 
				
			||||||
	assert.Equal(t, "", rd)
 | 
						assert.Equal(t, "", rd)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil)
 | 
					func TestCheckRedirectParam_ValidFull(t *testing.T) {
 | 
				
			||||||
 | 
						a := newTestApplication()
 | 
				
			||||||
 | 
						// Test valid full rd param
 | 
				
			||||||
 | 
						req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rd, ok = a.checkRedirectParam(req)
 | 
						rd, ok := a.checkRedirectParam(req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, true, ok)
 | 
				
			||||||
 | 
						assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCheckRedirectParam_ValidPartial(t *testing.T) {
 | 
				
			||||||
 | 
						a := newTestApplication()
 | 
				
			||||||
 | 
						// Test valid partial rd param
 | 
				
			||||||
 | 
						req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=/test?foo", nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rd, ok := a.checkRedirectParam(req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.Equal(t, true, ok)
 | 
						assert.Equal(t, true, ok)
 | 
				
			||||||
	assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)
 | 
						assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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")
 | 
				
			||||||
 | 
				
			|||||||
@ -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-08 00:09+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 ""
 | 
				
			||||||
 | 
				
			|||||||
										
											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-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: 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 错误"
 | 
				
			||||||
 | 
				
			|||||||
										
											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-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: 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 错误"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "name": "@goauthentik/authentik",
 | 
					    "name": "@goauthentik/authentik",
 | 
				
			||||||
    "version": "2024.6.4",
 | 
					    "version": "2024.8.2",
 | 
				
			||||||
    "private": true
 | 
					    "private": true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										575
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										575
									
								
								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]
 | 
				
			||||||
@ -1053,38 +1053,38 @@ toml = ["tomli"]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "cryptography"
 | 
					name = "cryptography"
 | 
				
			||||||
version = "43.0.0"
 | 
					version = "43.0.1"
 | 
				
			||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 | 
					description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 | 
				
			||||||
optional = false
 | 
					optional = false
 | 
				
			||||||
python-versions = ">=3.7"
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
files = [
 | 
					files = [
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
 | 
					    {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
 | 
					    {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
 | 
					    {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
 | 
					    {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
 | 
					    {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
 | 
					    {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
 | 
					    {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
 | 
					    {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
 | 
					    {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
 | 
				
			||||||
    {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
 | 
					    {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
 | 
				
			||||||
    {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
 | 
					    {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[package.dependencies]
 | 
					[package.dependencies]
 | 
				
			||||||
@ -1097,7 +1097,7 @@ nox = ["nox"]
 | 
				
			|||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
 | 
					pep8test = ["check-sdist", "click", "mypy", "ruff"]
 | 
				
			||||||
sdist = ["build"]
 | 
					sdist = ["build"]
 | 
				
			||||||
ssh = ["bcrypt (>=3.1.5)"]
 | 
					ssh = ["bcrypt (>=3.1.5)"]
 | 
				
			||||||
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
 | 
					test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
 | 
				
			||||||
test-randomorder = ["pytest-randomly"]
 | 
					test-randomorder = ["pytest-randomly"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -1165,15 +1165,18 @@ files = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[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.146.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.146.0-py2.py3-none-any.whl", hash = "sha256:b1e62c9889c5ef6022f11d30d7ef23dc55100300f0e8aaf8aa09e8e92540acad"},
 | 
				
			||||||
    {file = "google_api_python_client-2.142.0.tar.gz", hash = "sha256:a1101ac9e24356557ca22f07ff48b7f61fa5d4b4e7feeef3bda16e5dcb86350e"},
 | 
					    {file = "google_api_python_client-2.146.0.tar.gz", hash = "sha256:41f671be10fa077ee5143ee9f0903c14006d39dc644564f4e044ae96b380bf68"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[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.2"
 | 
				
			||||||
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.2-py3-none-any.whl", hash = "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2"},
 | 
				
			||||||
    {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"},
 | 
					    {file = "psycopg-3.2.2.tar.gz", hash = "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[package.dependencies]
 | 
					[package.dependencies]
 | 
				
			||||||
psycopg-c = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"c\""}
 | 
					psycopg-c = {version = "3.2.2", 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.2)"]
 | 
				
			||||||
c = ["psycopg-c (==3.2.1)"]
 | 
					c = ["psycopg-c (==3.2.2)"]
 | 
				
			||||||
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.2"
 | 
				
			||||||
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.2.tar.gz", hash = "sha256:de8cac75bc6640ef0f54ad9187b81e07c430206a83c566b73d4cca41ecccb7c8"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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.7"
 | 
				
			||||||
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.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
 | 
					    {file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
 | 
					    {file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
 | 
					    {file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
 | 
					    {file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
 | 
					    {file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
 | 
					    {file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
 | 
					    {file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
 | 
					    {file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
 | 
					    {file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"},
 | 
				
			||||||
    {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
 | 
					    {file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"},
 | 
				
			||||||
    {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
 | 
					    {file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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.1"
 | 
				
			||||||
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.1-py2.py3-none-any.whl", hash = "sha256:48c714e5a1340a3d9001d6c9ccd107434c340ff94a5bc1bfb2473d3dc33f5051"},
 | 
				
			||||||
    {file = "twilio-9.2.3.tar.gz", hash = "sha256:da2255b5f3753cb3bf647fc6c50edbdb367ebc3cde6802806f6f863058a65f75"},
 | 
					    {file = "twilio-9.3.1.tar.gz", hash = "sha256:bd754371353855438a8cf0c38f54580d474afe9655af2d81edb6dbabd8d785c4"},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[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]
 | 
				
			||||||
@ -4896,46 +4882,41 @@ files = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "watchdog"
 | 
					name = "watchdog"
 | 
				
			||||||
version = "4.0.2"
 | 
					version = "5.0.2"
 | 
				
			||||||
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.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d961f4123bb3c447d9fcdcb67e1530c366f10ab3a0c7d1c0c9943050936d4877"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"},
 | 
					    {file = "watchdog-5.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72990192cb63872c47d5e5fefe230a401b87fd59d257ee577d61c9e5564c62e5"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"},
 | 
					    {file = "watchdog-5.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bec703ad90b35a848e05e1b40bf0050da7ca28ead7ac4be724ae5ac2653a1a0"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"},
 | 
					    {file = "watchdog-5.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae7a1879918f6544201d33666909b040a46421054a50e0f773e0d870ed7438d"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"},
 | 
					    {file = "watchdog-5.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4a440f725f3b99133de610bfec93d570b13826f89616377715b9cd60424db6e"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"},
 | 
					    {file = "watchdog-5.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8b2918c19e0d48f5f20df458c84692e2a054f02d9df25e6c3c930063eca64c1"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"},
 | 
					    {file = "watchdog-5.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:aa9cd6e24126d4afb3752a3e70fce39f92d0e1a58a236ddf6ee823ff7dba28ee"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"},
 | 
					    {file = "watchdog-5.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f627c5bf5759fdd90195b0c0431f99cff4867d212a67b384442c51136a098ed7"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"},
 | 
					    {file = "watchdog-5.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7594a6d32cda2b49df3fd9abf9b37c8d2f3eab5df45c24056b4a671ac661619"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"},
 | 
					    {file = "watchdog-5.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba32efcccfe2c58f4d01115440d1672b4eb26cdd6fc5b5818f1fb41f7c3e1889"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"},
 | 
					    {file = "watchdog-5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:963f7c4c91e3f51c998eeff1b3fb24a52a8a34da4f956e470f4b068bb47b78ee"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"},
 | 
					    {file = "watchdog-5.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c47150aa12f775e22efff1eee9f0f6beee542a7aa1a985c271b1997d340184f"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"},
 | 
					    {file = "watchdog-5.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:14dd4ed023d79d1f670aa659f449bcd2733c33a35c8ffd88689d9d243885198b"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"},
 | 
					    {file = "watchdog-5.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b84bff0391ad4abe25c2740c7aec0e3de316fdf7764007f41e248422a7760a7f"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"},
 | 
					    {file = "watchdog-5.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e8d5ff39f0a9968952cce548e8e08f849141a4fcc1290b1c17c032ba697b9d7"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"},
 | 
					    {file = "watchdog-5.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fb223456db6e5f7bd9bbd5cd969f05aae82ae21acc00643b60d81c770abd402b"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"},
 | 
					    {file = "watchdog-5.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9814adb768c23727a27792c77812cf4e2fd9853cd280eafa2bcfa62a99e8bd6e"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"},
 | 
					    {file = "watchdog-5.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:901ee48c23f70193d1a7bc2d9ee297df66081dd5f46f0ca011be4f70dec80dab"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"},
 | 
					    {file = "watchdog-5.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:638bcca3d5b1885c6ec47be67bf712b00a9ab3d4b22ec0881f4889ad870bc7e8"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5597c051587f8757798216f2485e85eac583c3b343e9aa09127a3a6f82c65ee8"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:53ed1bf71fcb8475dd0ef4912ab139c294c87b903724b6f4a8bd98e026862e6d"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:29e4a2607bd407d9552c502d38b45a05ec26a8e40cc7e94db9bb48f861fa5abc"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:b6dc8f1d770a8280997e4beae7b9a75a33b268c59e033e72c8a10990097e5fde"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:d2ab34adc9bf1489452965cdb16a924e97d4452fcf88a50b21859068b50b5c3b"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:7d1aa7e4bb0f0c65a1a91ba37c10e19dabf7eaaa282c5787e51371f090748f4b"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"},
 | 
					    {file = "watchdog-5.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:726eef8f8c634ac6584f86c9c53353a010d9f311f6c15a034f3800a7a891d941"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"},
 | 
					    {file = "watchdog-5.0.2-py3-none-win32.whl", hash = "sha256:bda40c57115684d0216556671875e008279dea2dc00fcd3dde126ac8e0d7a2fb"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"},
 | 
					    {file = "watchdog-5.0.2-py3-none-win_amd64.whl", hash = "sha256:d010be060c996db725fbce7e3ef14687cdcc76f4ca0e4339a68cc4532c382a73"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"},
 | 
					    {file = "watchdog-5.0.2-py3-none-win_ia64.whl", hash = "sha256:3960136b2b619510569b90f0cd96408591d6c251a75c97690f4553ca88889769"},
 | 
				
			||||||
    {file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"},
 | 
					    {file = "watchdog-5.0.2.tar.gz", hash = "sha256:dcebf7e475001d2cdeb020be630dc5b687e9acdd60d16fea6bb4508e7b94cf76"},
 | 
				
			||||||
    {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.6.4"
 | 
					version = "2024.8.2"
 | 
				
			||||||
description = ""
 | 
					description = ""
 | 
				
			||||||
authors = ["authentik Team <hello@goauthentik.io>"]
 | 
					authors = ["authentik Team <hello@goauthentik.io>"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										113
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								schema.yml
									
									
									
									
									
								
							@ -1,7 +1,7 @@
 | 
				
			|||||||
openapi: 3.0.3
 | 
					openapi: 3.0.3
 | 
				
			||||||
info:
 | 
					info:
 | 
				
			||||||
  title: authentik
 | 
					  title: authentik
 | 
				
			||||||
  version: 2024.6.4
 | 
					  version: 2024.8.2
 | 
				
			||||||
  description: Making authentication simple.
 | 
					  description: Making authentication simple.
 | 
				
			||||||
  contact:
 | 
					  contact:
 | 
				
			||||||
    email: hello@goauthentik.io
 | 
					    email: hello@goauthentik.io
 | 
				
			||||||
@ -290,6 +290,64 @@ paths:
 | 
				
			|||||||
              schema:
 | 
					              schema:
 | 
				
			||||||
                $ref: '#/components/schemas/GenericError'
 | 
					                $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
          description: ''
 | 
					          description: ''
 | 
				
			||||||
 | 
					  /analytics/data/:
 | 
				
			||||||
 | 
					    get:
 | 
				
			||||||
 | 
					      operationId: analytics_data_list
 | 
				
			||||||
 | 
					      description: Read-only view of analytics descriptions
 | 
				
			||||||
 | 
					      tags:
 | 
				
			||||||
 | 
					      - analytics
 | 
				
			||||||
 | 
					      security:
 | 
				
			||||||
 | 
					      - authentik: []
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                type: array
 | 
				
			||||||
 | 
					                items:
 | 
				
			||||||
 | 
					                  $ref: '#/components/schemas/AnalyticsData'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
 | 
					        '400':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/ValidationError'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
 | 
					        '403':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
 | 
					  /analytics/description/:
 | 
				
			||||||
 | 
					    get:
 | 
				
			||||||
 | 
					      operationId: analytics_description_list
 | 
				
			||||||
 | 
					      description: Read-only view of analytics descriptions
 | 
				
			||||||
 | 
					      tags:
 | 
				
			||||||
 | 
					      - analytics
 | 
				
			||||||
 | 
					      security:
 | 
				
			||||||
 | 
					      - authentik: []
 | 
				
			||||||
 | 
					      responses:
 | 
				
			||||||
 | 
					        '200':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                type: array
 | 
				
			||||||
 | 
					                items:
 | 
				
			||||||
 | 
					                  $ref: '#/components/schemas/AnalyticsDescription'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
 | 
					        '400':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/ValidationError'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
 | 
					        '403':
 | 
				
			||||||
 | 
					          content:
 | 
				
			||||||
 | 
					            application/json:
 | 
				
			||||||
 | 
					              schema:
 | 
				
			||||||
 | 
					                $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
 | 
					          description: ''
 | 
				
			||||||
  /authenticators/admin/all/:
 | 
					  /authenticators/admin/all/:
 | 
				
			||||||
    get:
 | 
					    get:
 | 
				
			||||||
      operationId: authenticators_admin_all_list
 | 
					      operationId: authenticators_admin_all_list
 | 
				
			||||||
@ -11502,6 +11560,7 @@ paths:
 | 
				
			|||||||
          type: string
 | 
					          type: string
 | 
				
			||||||
          nullable: true
 | 
					          nullable: true
 | 
				
			||||||
          enum:
 | 
					          enum:
 | 
				
			||||||
 | 
					          - analytics_sent
 | 
				
			||||||
          - authorize_application
 | 
					          - authorize_application
 | 
				
			||||||
          - configuration_error
 | 
					          - configuration_error
 | 
				
			||||||
          - custom_
 | 
					          - custom_
 | 
				
			||||||
@ -35758,6 +35817,25 @@ components:
 | 
				
			|||||||
      - rsa
 | 
					      - rsa
 | 
				
			||||||
      - ecdsa
 | 
					      - ecdsa
 | 
				
			||||||
      type: string
 | 
					      type: string
 | 
				
			||||||
 | 
					    AnalyticsData:
 | 
				
			||||||
 | 
					      type: object
 | 
				
			||||||
 | 
					      properties:
 | 
				
			||||||
 | 
					        data:
 | 
				
			||||||
 | 
					          type: object
 | 
				
			||||||
 | 
					          additionalProperties: {}
 | 
				
			||||||
 | 
					      required:
 | 
				
			||||||
 | 
					      - data
 | 
				
			||||||
 | 
					    AnalyticsDescription:
 | 
				
			||||||
 | 
					      type: object
 | 
				
			||||||
 | 
					      description: Base serializer class which doesn't implement create/update methods
 | 
				
			||||||
 | 
					      properties:
 | 
				
			||||||
 | 
					        label:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        desc:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					      required:
 | 
				
			||||||
 | 
					      - desc
 | 
				
			||||||
 | 
					      - label
 | 
				
			||||||
    App:
 | 
					    App:
 | 
				
			||||||
      type: object
 | 
					      type: object
 | 
				
			||||||
      description: Serialize Application info
 | 
					      description: Serialize Application info
 | 
				
			||||||
@ -35773,6 +35851,7 @@ components:
 | 
				
			|||||||
      enum:
 | 
					      enum:
 | 
				
			||||||
      - authentik.tenants
 | 
					      - authentik.tenants
 | 
				
			||||||
      - authentik.admin
 | 
					      - authentik.admin
 | 
				
			||||||
 | 
					      - authentik.analytics
 | 
				
			||||||
      - authentik.api
 | 
					      - authentik.api
 | 
				
			||||||
      - authentik.crypto
 | 
					      - authentik.crypto
 | 
				
			||||||
      - authentik.flows
 | 
					      - authentik.flows
 | 
				
			||||||
@ -38960,6 +39039,7 @@ components:
 | 
				
			|||||||
      - model_updated
 | 
					      - model_updated
 | 
				
			||||||
      - model_deleted
 | 
					      - model_deleted
 | 
				
			||||||
      - email_sent
 | 
					      - email_sent
 | 
				
			||||||
 | 
					      - analytics_sent
 | 
				
			||||||
      - update_available
 | 
					      - update_available
 | 
				
			||||||
      - custom_
 | 
					      - custom_
 | 
				
			||||||
      type: string
 | 
					      type: string
 | 
				
			||||||
@ -47361,6 +47441,13 @@ components:
 | 
				
			|||||||
          maximum: 2147483647
 | 
					          maximum: 2147483647
 | 
				
			||||||
          minimum: 1
 | 
					          minimum: 1
 | 
				
			||||||
          description: Default token length
 | 
					          description: Default token length
 | 
				
			||||||
 | 
					        analytics_enabled:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
 | 
					        analytics_sources:
 | 
				
			||||||
 | 
					          type: array
 | 
				
			||||||
 | 
					          items:
 | 
				
			||||||
 | 
					            type: string
 | 
				
			||||||
 | 
					            minLength: 1
 | 
				
			||||||
    PatchedSourceStageRequest:
 | 
					    PatchedSourceStageRequest:
 | 
				
			||||||
      type: object
 | 
					      type: object
 | 
				
			||||||
      description: SourceStage Serializer
 | 
					      description: SourceStage Serializer
 | 
				
			||||||
@ -51001,6 +51088,23 @@ components:
 | 
				
			|||||||
          maximum: 2147483647
 | 
					          maximum: 2147483647
 | 
				
			||||||
          minimum: 1
 | 
					          minimum: 1
 | 
				
			||||||
          description: Default token length
 | 
					          description: Default token length
 | 
				
			||||||
 | 
					        analytics_enabled:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
 | 
					        analytics_sources:
 | 
				
			||||||
 | 
					          type: array
 | 
				
			||||||
 | 
					          items:
 | 
				
			||||||
 | 
					            type: string
 | 
				
			||||||
 | 
					        analytics_sources_obj:
 | 
				
			||||||
 | 
					          type: array
 | 
				
			||||||
 | 
					          items:
 | 
				
			||||||
 | 
					            type: array
 | 
				
			||||||
 | 
					            items:
 | 
				
			||||||
 | 
					              type: string
 | 
				
			||||||
 | 
					            minLength: 2
 | 
				
			||||||
 | 
					            maxLength: 2
 | 
				
			||||||
 | 
					          readOnly: true
 | 
				
			||||||
 | 
					      required:
 | 
				
			||||||
 | 
					      - analytics_sources_obj
 | 
				
			||||||
    SettingsRequest:
 | 
					    SettingsRequest:
 | 
				
			||||||
      type: object
 | 
					      type: object
 | 
				
			||||||
      description: Settings Serializer
 | 
					      description: Settings Serializer
 | 
				
			||||||
@ -51041,6 +51145,13 @@ components:
 | 
				
			|||||||
          maximum: 2147483647
 | 
					          maximum: 2147483647
 | 
				
			||||||
          minimum: 1
 | 
					          minimum: 1
 | 
				
			||||||
          description: Default token length
 | 
					          description: Default token length
 | 
				
			||||||
 | 
					        analytics_enabled:
 | 
				
			||||||
 | 
					          type: boolean
 | 
				
			||||||
 | 
					        analytics_sources:
 | 
				
			||||||
 | 
					          type: array
 | 
				
			||||||
 | 
					          items:
 | 
				
			||||||
 | 
					            type: string
 | 
				
			||||||
 | 
					            minLength: 1
 | 
				
			||||||
    SeverityEnum:
 | 
					    SeverityEnum:
 | 
				
			||||||
      enum:
 | 
					      enum:
 | 
				
			||||||
      - notice
 | 
					      - notice
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ from ldap3.core.exceptions import LDAPInvalidCredentialsResult
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.blueprints.tests import apply_blueprint, reconcile_app
 | 
					from authentik.blueprints.tests import apply_blueprint, reconcile_app
 | 
				
			||||||
from authentik.core.models import Application, User
 | 
					from authentik.core.models import Application, User
 | 
				
			||||||
 | 
					from authentik.core.tests.utils import create_test_user
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.flows.models import Flow
 | 
					from authentik.flows.models import Flow
 | 
				
			||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
@ -331,6 +332,83 @@ class TestProviderLDAP(SeleniumTestCase):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
        self.assert_list_dict_equal(expected, response)
 | 
					        self.assert_list_dict_equal(expected, response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @retry()
 | 
				
			||||||
 | 
					    @apply_blueprint(
 | 
				
			||||||
 | 
					        "default/flow-default-authentication-flow.yaml",
 | 
				
			||||||
 | 
					        "default/flow-default-invalidation-flow.yaml",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @reconcile_app("authentik_tenants")
 | 
				
			||||||
 | 
					    @reconcile_app("authentik_outposts")
 | 
				
			||||||
 | 
					    def test_ldap_bind_search_no_perms(self):
 | 
				
			||||||
 | 
					        """Test simple bind + search"""
 | 
				
			||||||
 | 
					        user = create_test_user()
 | 
				
			||||||
 | 
					        self._prepare()
 | 
				
			||||||
 | 
					        server = Server("ldap://localhost:3389", get_info=ALL)
 | 
				
			||||||
 | 
					        _connection = Connection(
 | 
				
			||||||
 | 
					            server,
 | 
				
			||||||
 | 
					            raise_exceptions=True,
 | 
				
			||||||
 | 
					            user=f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
 | 
				
			||||||
 | 
					            password=user.username,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        _connection.bind()
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.LOGIN,
 | 
				
			||||||
 | 
					                user={
 | 
				
			||||||
 | 
					                    "pk": user.pk,
 | 
				
			||||||
 | 
					                    "email": user.email,
 | 
				
			||||||
 | 
					                    "username": user.username,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _connection.search(
 | 
				
			||||||
 | 
					            "ou=Users,DC=ldaP,dc=goauthentik,dc=io",
 | 
				
			||||||
 | 
					            "(objectClass=user)",
 | 
				
			||||||
 | 
					            search_scope=SUBTREE,
 | 
				
			||||||
 | 
					            attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response: list = _connection.response
 | 
				
			||||||
 | 
					        # Remove raw_attributes to make checking easier
 | 
				
			||||||
 | 
					        for obj in response:
 | 
				
			||||||
 | 
					            del obj["raw_attributes"]
 | 
				
			||||||
 | 
					            del obj["raw_dn"]
 | 
				
			||||||
 | 
					            obj["attributes"] = dict(obj["attributes"])
 | 
				
			||||||
 | 
					        expected = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "dn": f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
 | 
				
			||||||
 | 
					                "attributes": {
 | 
				
			||||||
 | 
					                    "cn": user.username,
 | 
				
			||||||
 | 
					                    "sAMAccountName": user.username,
 | 
				
			||||||
 | 
					                    "uid": user.uid,
 | 
				
			||||||
 | 
					                    "name": user.name,
 | 
				
			||||||
 | 
					                    "displayName": user.name,
 | 
				
			||||||
 | 
					                    "sn": user.name,
 | 
				
			||||||
 | 
					                    "mail": user.email,
 | 
				
			||||||
 | 
					                    "objectClass": [
 | 
				
			||||||
 | 
					                        "top",
 | 
				
			||||||
 | 
					                        "person",
 | 
				
			||||||
 | 
					                        "organizationalPerson",
 | 
				
			||||||
 | 
					                        "inetOrgPerson",
 | 
				
			||||||
 | 
					                        "user",
 | 
				
			||||||
 | 
					                        "posixAccount",
 | 
				
			||||||
 | 
					                        "goauthentik.io/ldap/user",
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    "uidNumber": 2000 + user.pk,
 | 
				
			||||||
 | 
					                    "gidNumber": 2000 + user.pk,
 | 
				
			||||||
 | 
					                    "memberOf": [
 | 
				
			||||||
 | 
					                        f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
 | 
				
			||||||
 | 
					                        for group in user.ak_groups.all()
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    "homeDirectory": f"/home/{user.username}",
 | 
				
			||||||
 | 
					                    "ak-active": True,
 | 
				
			||||||
 | 
					                    "ak-superuser": False,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                "type": "searchResEntry",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        self.assert_list_dict_equal(expected, response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
 | 
					    def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
 | 
				
			||||||
        """Assert a list of dictionaries is identical, ignoring the ordering of items"""
 | 
					        """Assert a list of dictionaries is identical, ignoring the ordering of items"""
 | 
				
			||||||
        self.assertEqual(len(expected), len(actual))
 | 
					        self.assertEqual(len(expected), len(actual))
 | 
				
			||||||
 | 
				
			|||||||
@ -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": "^_"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,26 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
    "env": {
 | 
					 | 
				
			||||||
        "browser": true,
 | 
					 | 
				
			||||||
        "es2021": true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "extends": [
 | 
					 | 
				
			||||||
        "eslint:recommended",
 | 
					 | 
				
			||||||
        "plugin:@typescript-eslint/recommended",
 | 
					 | 
				
			||||||
        "plugin:sonarjs/recommended"
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    "parser": "@typescript-eslint/parser",
 | 
					 | 
				
			||||||
    "parserOptions": {
 | 
					 | 
				
			||||||
        "ecmaVersion": 12,
 | 
					 | 
				
			||||||
        "sourceType": "module"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "plugins": ["@typescript-eslint", "sonarjs"],
 | 
					 | 
				
			||||||
    "rules": {
 | 
					 | 
				
			||||||
        "indent": "off",
 | 
					 | 
				
			||||||
        "linebreak-style": ["error", "unix"],
 | 
					 | 
				
			||||||
        "quotes": ["error", "double", { "avoidEscape": true }],
 | 
					 | 
				
			||||||
        "semi": ["error", "always"],
 | 
					 | 
				
			||||||
        "@typescript-eslint/ban-ts-comment": "off",
 | 
					 | 
				
			||||||
        "sonarjs/cognitive-complexity": ["error", 9],
 | 
					 | 
				
			||||||
        "sonarjs/no-nested-template-literals": "off"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										84
									
								
								tests/wdio/eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								tests/wdio/eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					import eslint from "@eslint/js";
 | 
				
			||||||
 | 
					import tsparser from "@typescript-eslint/parser";
 | 
				
			||||||
 | 
					import litconf from "eslint-plugin-lit";
 | 
				
			||||||
 | 
					import wcconf from "eslint-plugin-wc";
 | 
				
			||||||
 | 
					import globals from "globals";
 | 
				
			||||||
 | 
					import tseslint from "typescript-eslint";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default [
 | 
				
			||||||
 | 
					    // You would not believe how much this change has frustrated users: ["if an ignores key is used
 | 
				
			||||||
 | 
					    // without any other keys in the configuration object, then the patterns act as global
 | 
				
			||||||
 | 
					    // ignores"](https://eslint.org/docs/latest/use/configure/ignore)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ignores: [
 | 
				
			||||||
 | 
					            "dist/",
 | 
				
			||||||
 | 
					            // don't lint the cache
 | 
				
			||||||
 | 
					            ".wireit/",
 | 
				
			||||||
 | 
					            // let packages have their own configurations
 | 
				
			||||||
 | 
					            "packages/",
 | 
				
			||||||
 | 
					            // don't ever lint node_modules
 | 
				
			||||||
 | 
					            "node_modules/",
 | 
				
			||||||
 | 
					            ".storybook/*",
 | 
				
			||||||
 | 
					            // don't lint build output (make sure it's set to your correct build folder name)
 | 
				
			||||||
 | 
					            // don't lint nyc coverage output
 | 
				
			||||||
 | 
					            "coverage/",
 | 
				
			||||||
 | 
					            "src/locale-codes.ts",
 | 
				
			||||||
 | 
					            "storybook-static/",
 | 
				
			||||||
 | 
					            "src/locales/",
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    eslint.configs.recommended,
 | 
				
			||||||
 | 
					    wcconf.configs["flat/recommended"],
 | 
				
			||||||
 | 
					    litconf.configs["flat/recommended"],
 | 
				
			||||||
 | 
					    ...tseslint.configs.recommended,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        languageOptions: {
 | 
				
			||||||
 | 
					            parser: tsparser,
 | 
				
			||||||
 | 
					            parserOptions: {
 | 
				
			||||||
 | 
					                ecmaVersion: 12,
 | 
				
			||||||
 | 
					                sourceType: "module",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        files: ["src/**"],
 | 
				
			||||||
 | 
					        rules: {
 | 
				
			||||||
 | 
					            "no-unused-vars": "off",
 | 
				
			||||||
 | 
					            "no-console": ["error", { allow: ["debug", "warn", "error"] }],
 | 
				
			||||||
 | 
					            "@typescript-eslint/ban-ts-comment": "off",
 | 
				
			||||||
 | 
					            "@typescript-eslint/no-unused-vars": [
 | 
				
			||||||
 | 
					                "error",
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    argsIgnorePattern: "^_",
 | 
				
			||||||
 | 
					                    varsIgnorePattern: "^_",
 | 
				
			||||||
 | 
					                    caughtErrorsIgnorePattern: "^_",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        languageOptions: {
 | 
				
			||||||
 | 
					            parser: tsparser,
 | 
				
			||||||
 | 
					            parserOptions: {
 | 
				
			||||||
 | 
					                ecmaVersion: 12,
 | 
				
			||||||
 | 
					                sourceType: "module",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            globals: {
 | 
				
			||||||
 | 
					                ...globals.nodeBuiltin,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        files: ["scripts/*.mjs", "*.ts", "*.mjs"],
 | 
				
			||||||
 | 
					        rules: {
 | 
				
			||||||
 | 
					            "no-unused-vars": "off",
 | 
				
			||||||
 | 
					            // We WANT our scripts to output to the console!
 | 
				
			||||||
 | 
					            "no-console": "off",
 | 
				
			||||||
 | 
					            "@typescript-eslint/ban-ts-comment": "off",
 | 
				
			||||||
 | 
					            "@typescript-eslint/no-unused-vars": [
 | 
				
			||||||
 | 
					                "error",
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    argsIgnorePattern: "^_",
 | 
				
			||||||
 | 
					                    varsIgnorePattern: "^_",
 | 
				
			||||||
 | 
					                    caughtErrorsIgnorePattern: "^_",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
							
								
								
									
										10691
									
								
								tests/wdio/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10691
									
								
								tests/wdio/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,37 +1,44 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "name": "@goauthentik/web-tests",
 | 
					    "name": "@goauthentik/web-tests",
 | 
				
			||||||
    "private": true,
 | 
					    "dependencies": {
 | 
				
			||||||
    "type": "module",
 | 
					        "chromedriver": "^129.0.0",
 | 
				
			||||||
 | 
					        "lockfile-lint": "^4.14.0",
 | 
				
			||||||
 | 
					        "syncpack": "^13.0.0"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "devDependencies": {
 | 
					    "devDependencies": {
 | 
				
			||||||
 | 
					        "@eslint/js": "^9.11.1",
 | 
				
			||||||
        "@trivago/prettier-plugin-sort-imports": "^4.3.0",
 | 
					        "@trivago/prettier-plugin-sort-imports": "^4.3.0",
 | 
				
			||||||
        "@typescript-eslint/eslint-plugin": "^7.17.0",
 | 
					        "@types/mocha": "^10.0.8",
 | 
				
			||||||
        "@typescript-eslint/parser": "^7.17.0",
 | 
					        "@typescript-eslint/eslint-plugin": "^8.7.0",
 | 
				
			||||||
 | 
					        "@typescript-eslint/parser": "^8.7.0",
 | 
				
			||||||
        "@wdio/cli": "^9.0.3",
 | 
					        "@wdio/cli": "^9.0.3",
 | 
				
			||||||
        "@wdio/local-runner": "^9.0.1",
 | 
					        "@wdio/local-runner": "^9.0.1",
 | 
				
			||||||
        "@wdio/mocha-framework": "^8.40.2",
 | 
					        "@wdio/mocha-framework": "^9.0.8",
 | 
				
			||||||
        "@wdio/spec-reporter": "^8.39.0",
 | 
					        "@wdio/spec-reporter": "^9.0.8",
 | 
				
			||||||
        "eslint": "^8.57.0",
 | 
					        "eslint-plugin-lit": "^1.14.0",
 | 
				
			||||||
        "eslint-config-google": "^0.14.0",
 | 
					        "eslint-plugin-sonarjs": "^2.0.2",
 | 
				
			||||||
        "eslint-plugin-sonarjs": "^1.0.3",
 | 
					        "eslint-plugin-wc": "^2.1.0",
 | 
				
			||||||
 | 
					        "eslint": "^9.11.1",
 | 
				
			||||||
        "npm-run-all": "^4.1.5",
 | 
					        "npm-run-all": "^4.1.5",
 | 
				
			||||||
        "prettier": "^3.3.3",
 | 
					        "prettier": "^3.3.3",
 | 
				
			||||||
        "ts-node": "^10.9.2",
 | 
					        "typescript-eslint": "^8.7.0",
 | 
				
			||||||
        "typescript": "^5.5.4",
 | 
					        "typescript": "^5.6.2",
 | 
				
			||||||
        "wdio-wait-for": "^3.0.11"
 | 
					        "wdio-wait-for": "^3.0.11"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "scripts": {
 | 
					 | 
				
			||||||
        "wdio": "wdio run ./wdio.conf.ts",
 | 
					 | 
				
			||||||
        "lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
 | 
					 | 
				
			||||||
        "lint": "eslint . --max-warnings 0 --fix",
 | 
					 | 
				
			||||||
        "lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
 | 
					 | 
				
			||||||
        "precommit": "run-s lint:precommit lint:spelling prettier",
 | 
					 | 
				
			||||||
        "prettier-check": "prettier --check .",
 | 
					 | 
				
			||||||
        "prettier": "prettier --write ."
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "engines": {
 | 
					    "engines": {
 | 
				
			||||||
        "node": ">=20"
 | 
					        "node": ">=20"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "dependencies": {
 | 
					    "private": true,
 | 
				
			||||||
        "chromedriver": "^128.0.0"
 | 
					    "scripts": {
 | 
				
			||||||
    }
 | 
					        "lint": "eslint . --max-warnings 0 --fix",
 | 
				
			||||||
 | 
					        "lint:lockfile": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https",
 | 
				
			||||||
 | 
					        "lint:package": "syncpack format -i '    '",
 | 
				
			||||||
 | 
					        "lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
 | 
				
			||||||
 | 
					        "lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
 | 
				
			||||||
 | 
					        "precommit": "run-s lint:precommit lint:spelling prettier",
 | 
				
			||||||
 | 
					        "prettier": "prettier --write .",
 | 
				
			||||||
 | 
					        "prettier-check": "prettier --check .",
 | 
				
			||||||
 | 
					        "wdio": "wdio run ./wdio.conf.ts"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "type": "module"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,11 @@
 | 
				
			|||||||
import Page from "../pageobjects/page.js";
 | 
					import Page from "../pageobjects/page.js";
 | 
				
			||||||
import { browser } from "@wdio/globals";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const CLICK_TIME_DELAY = 250;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class AdminPage extends Page {
 | 
					export default class AdminPage extends Page {
 | 
				
			||||||
    public get pageHeader() {
 | 
					    public async pageHeader() {
 | 
				
			||||||
        return $('>>>ak-page-header slot[name="header"]');
 | 
					        return await $("ak-page-header").$('slot[name="header"]');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async openApplicationsListPage() {
 | 
					    async openApplicationsListPage() {
 | 
				
			||||||
        await this.open("if/admin/#/core/applications");
 | 
					        await this.open("if/admin/#/core/applications");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public open(path: string) {
 | 
					 | 
				
			||||||
        return browser.url(`http://localhost:9000/${path}`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public pause(selector?: string) {
 | 
					 | 
				
			||||||
        if (selector) {
 | 
					 | 
				
			||||||
            return $(selector).waitForDisplayed();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return browser.pause(CLICK_TIME_DELAY);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user