Compare commits
	
		
			385 Commits
		
	
	
		
			version/20
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 619203c177 | |||
| 4ae476e58d | |||
| e444d0d640 | |||
| 3869965b4c | |||
| d4e1b95991 | |||
| 2b730dec54 | |||
| 2aacb311bc | |||
| 40055ef01b | |||
| 75608dce5c | |||
| b0f7083879 | |||
| 62bf79ce32 | |||
| 7a16c9cb14 | |||
| d29d161ac6 | |||
| 653631ac77 | |||
| cde303e780 | |||
| aa359a032c | |||
| 6491065aab | |||
| 79eec5a3a0 | |||
| cd5e091937 | |||
| 7ed8952803 | |||
| c1f302fb7c | |||
| cb31e52d0e | |||
| 782764ac73 | |||
| d0c56325ef | |||
| 73d57d6f82 | |||
| 2716a26887 | |||
| 0452537e8b | |||
| d1a1bfbbc5 | |||
| a69fcbca9a | |||
| 1ac4dacc3b | |||
| bcf7e162a4 | |||
| f44956bd61 | |||
| cb37e5c10e | |||
| 73bb778d62 | |||
| b612a82e16 | |||
| 83991c743e | |||
| 09f43ca43b | |||
| 1c91835a26 | |||
| c032914092 | |||
| 3634bf4629 | |||
| 45f99fbaf0 | |||
| e31a3307b5 | |||
| d28fcca344 | |||
| c296e1214c | |||
| d676cf6e3f | |||
| 39d87841d0 | |||
| fcd879034c | |||
| b285814e24 | |||
| 1a6ea72c09 | |||
| c251b87f8c | |||
| 21a9aa229a | |||
| 5f6565ee27 | |||
| afad55a357 | |||
| f25d76fa43 | |||
| 10b45d954e | |||
| 339eaf37f2 | |||
| f723fdd551 | |||
| 8359f0bfb3 | |||
| ee610a906a | |||
| 828eeb5ebb | |||
| c9c177d8f9 | |||
| c19afa4f16 | |||
| 941bc61b31 | |||
| 282b364606 | |||
| ad4bc4083d | |||
| ebe282eb1a | |||
| 830c26ca25 | |||
| ed3b4a3d4a | |||
| 975c4ddc04 | |||
| 7e2896298a | |||
| cba9cf8361 | |||
| bf12580f64 | |||
| 75ef4ce596 | |||
| c2f3ce11b0 | |||
| 3c256fecc6 | |||
| 0285b84133 | |||
| 99a371a02c | |||
| c7e6eb8896 | |||
| 674bd9e05c | |||
| b79901df87 | |||
| b248f450dd | |||
| 05db9e5c40 | |||
| 234a5e2b66 | |||
| aea1736f70 | |||
| 9f4a4449f5 | |||
| b6b55e2336 | |||
| 8f2805e05b | |||
| 4f3583cd7e | |||
| 617e90dca3 | |||
| f7408626a8 | |||
| 4dcb15af46 | |||
| 89beb7a9f7 | |||
| 28eeb4798e | |||
| 79b92e764e | |||
| 919336a519 | |||
| 27e04589c1 | |||
| ba44fbdac8 | |||
| 0e093a8917 | |||
| d0bfb99859 | |||
| 93bdea3769 | |||
| e681654af7 | |||
| cab7593dca | |||
| cf92f9aefc | |||
| 8d72b3498d | |||
| 42ab858c50 | |||
| a1abae9ab1 | |||
| 8f36b49061 | |||
| 64b4e851ce | |||
| 40a62ac1e5 | |||
| 5df60e4d87 | |||
| 50ebc8522d | |||
| eddca478dc | |||
| 99a7fca08e | |||
| a7e3602908 | |||
| 74169860cf | |||
| 52bb774f73 | |||
| f26fcaf825 | |||
| b8e92e2f11 | |||
| 08adfc94d6 | |||
| 236fafb735 | |||
| 5ad9ddee3c | |||
| 24d220ff49 | |||
| 3364c195b7 | |||
| 50aa87d141 | |||
| 72b375023d | |||
| 77ba186818 | |||
| 2fe6de0505 | |||
| bf9e969b53 | |||
| 184f119b16 | |||
| ebc06f1abe | |||
| 0f8880ab0a | |||
| ee56da5092 | |||
| 2152004502 | |||
| 45d0b80d02 | |||
| 96065eb942 | |||
| ac944fee8b | |||
| 1d0e5fc353 | |||
| 1f97420207 | |||
| ae07f13a87 | |||
| 0aec504170 | |||
| 3b4c9bcc57 | |||
| 5182a6741e | |||
| da7635ae5c | |||
| a92a0fb60a | |||
| cb10c1753b | |||
| ae654bd4c8 | |||
| 28192655ec | |||
| 9582294eb8 | |||
| 0172430d7d | |||
| 1454b65933 | |||
| 432a7792e2 | |||
| 54069618b4 | |||
| 81feb313df | |||
| e6b275add3 | |||
| 27016a5527 | |||
| 4c29d517f0 | |||
| 180d27cc37 | |||
| 5a8b356dc7 | |||
| 3195640776 | |||
| f463296d47 | |||
| adf4b23c01 | |||
| d900a2b6a9 | |||
| 95a2fddfa8 | |||
| 8f7d21b692 | |||
| 3f84abec2f | |||
| b5c857aff4 | |||
| f8dee09107 | |||
| 84a800583c | |||
| 88de94f014 | |||
| 25549ec339 | |||
| fe4923bff6 | |||
| bb1a0b6bd2 | |||
| 879b5ead71 | |||
| 1670ec9167 | |||
| ac52667327 | |||
| 0d7c5c2108 | |||
| 73e3d19384 | |||
| f6e0f0282d | |||
| 3f42067a8f | |||
| ed6f5b98df | |||
| dd290e264c | |||
| c85484fc00 | |||
| 663dffd8be | |||
| c15d0c3d17 | |||
| bf09a54f35 | |||
| 930dd51663 | |||
| 12a523c7aa | |||
| ea9a6d57dd | |||
| 91958e1232 | |||
| 8925afb089 | |||
| ccafe7be4f | |||
| 8279690a8f | |||
| 763d3ae76a | |||
| b775e7f4d3 | |||
| 3d8d93ece5 | |||
| 06af306e8a | |||
| 9257f3c919 | |||
| 2fe7f4cf04 | |||
| 04399bc8bb | |||
| fcbcfbc3c0 | |||
| 3e4ce62dfe | |||
| d8292151e6 | |||
| 3d01a59b34 | |||
| 5df15c4105 | |||
| 75d695105d | |||
| 28189bdddf | |||
| f6885c7cf8 | |||
| 2c43f0824e | |||
| 13e2eea72f | |||
| 9441be1ee2 | |||
| d7ab2a362a | |||
| e920be3a72 | |||
| f771383c4b | |||
| 65c75f085a | |||
| 17503365f7 | |||
| ebf9f0ca63 | |||
| ae26d2756f | |||
| 124071f9be | |||
| 471f7d9c62 | |||
| a6a6b3bd06 | |||
| 48ad3dccda | |||
| 341c58a722 | |||
| 9b04f2da48 | |||
| f7a296544f | |||
| 78641a57ad | |||
| a77ff5ffec | |||
| bdd5e16db1 | |||
| d4672bfe79 | |||
| abd9fab41a | |||
| 7c8bf42ef9 | |||
| 274b555912 | |||
| 916530f0d8 | |||
| 95efd47f65 | |||
| 90ecb1af7f | |||
| d7fdca1b44 | |||
| 37346763dc | |||
| c35fd2755f | |||
| 281e3a0518 | |||
| 6349cdad2f | |||
| ef341dd405 | |||
| 198e5ce642 | |||
| 923fbac5b0 | |||
| 5f28c7ace7 | |||
| d96c96006f | |||
| 3ddf2d6f85 | |||
| ba6849f29c | |||
| 942170f902 | |||
| 248f993541 | |||
| 56d40bddd0 | |||
| 3a700a449a | |||
| a20f552bcf | |||
| 32331a56eb | |||
| d752b7e41c | |||
| 0b4223c6ca | |||
| a3ec5c13f0 | |||
| 128b582dd6 | |||
| e59ede5422 | |||
| 6d08ba2513 | |||
| 23444f4df0 | |||
| 3338f7a401 | |||
| b126519275 | |||
| 71e68b498e | |||
| fb267ee223 | |||
| 8e59b06611 | |||
| a4b3519428 | |||
| 4895fc3bbb | |||
| 3daabd6fa8 | |||
| 9fccb14065 | |||
| 12efe94fd1 | |||
| 375ef27b9f | |||
| 9a7fa39de4 | |||
| c779ad2e3b | |||
| 7e7ef289ba | |||
| 223d9ad414 | |||
| 948ea7b087 | |||
| bf771f8b6c | |||
| 6dc8aa396c | |||
| 92a48f9dc6 | |||
| d0ad9fcb1f | |||
| 539e6deca5 | |||
| df4c8003b8 | |||
| 169e748a78 | |||
| 39b365c6ae | |||
| 9a79bab43d | |||
| e229eda96e | |||
| 4448145aa9 | |||
| 3d042e708a | |||
| 2428d5f1c2 | |||
| f1dc2b4d2a | |||
| 7dfbcdbb81 | |||
| 5fd4f56fa2 | |||
| b9d5ba6b0a | |||
| 2a4cb07ba8 | |||
| 7939286176 | |||
| 46ef49b897 | |||
| b923d85f6a | |||
| 2862b4ecfb | |||
| 094acc62f0 | |||
| 13d17dc729 | |||
| 5cf3a13ca8 | |||
| d0898a3869 | |||
| 7158c9d2ea | |||
| c5cf17b60b | |||
| da58796768 | |||
| d98499a3fa | |||
| e5944567e8 | |||
| d296c12d01 | |||
| 4c3a9e69f2 | |||
| eb2540a3c8 | |||
| bf9a3615d9 | |||
| 33fb22e3e7 | |||
| f3ff398a44 | |||
| 533eb59a04 | |||
| 8ca29f6d49 | |||
| 0a33d38adf | |||
| f7afb60c1f | |||
| b9c605bf1a | |||
| 2983adc719 | |||
| 502393ee56 | |||
| 121bba1d9f | |||
| 3c1b70c355 | |||
| 27508dd1f0 | |||
| 6d962dbdf3 | |||
| 9194e6368a | |||
| 917fb7d626 | |||
| 3cf5794b96 | |||
| 631b0a1819 | |||
| 6662dcc4b0 | |||
| 95db54b819 | |||
| bc7d5042df | |||
| de3e1c3dbc | |||
| 3c6aac5435 | |||
| eeb755ab7d | |||
| 70d0dd51a5 | |||
| 073dd8b560 | |||
| b5d2924d46 | |||
| 597e279f34 | |||
| fc28def83d | |||
| f6efdfded4 | |||
| 91312496e0 | |||
| b557b4337d | |||
| bfde186aa0 | |||
| 2bd75dd1a9 | |||
| 27ab31a9b0 | |||
| 44a8b737d9 | |||
| b939ee7a09 | |||
| 0bae550520 | |||
| b5cc2f2bda | |||
| 9ad4cf1db9 | |||
| 9dbafaaea2 | |||
| 2db8b07578 | |||
| 7c1a7bfd9d | |||
| b7ef076798 | |||
| 37c29a073e | |||
| 0c288ea64b | |||
| 2476475174 | |||
| 71913c8164 | |||
| 6ec8432217 | |||
| 7a12c0e4d1 | |||
| 23a7eba16b | |||
| 3ba84a8e8b | |||
| 75476217a0 | |||
| 7771c0b905 | |||
| 3378e82ec7 | |||
| 126e43dea4 | |||
| f725009530 | |||
| 70d1e3a0cb | |||
| e751ce1220 | |||
| e09a27cf87 | |||
| 06fbf44724 | |||
| 200e409d91 | |||
| 5e5854e256 | |||
| 3df8bcfc9c | |||
| e76c14f9e0 | |||
| 6b6748b1c7 | |||
| d92d8e6dbb | |||
| c2b9dc5c75 | |||
| 5c1d27de2b | |||
| 6ab9e7cd68 | |||
| 3ef56e9ec1 | |||
| 6d8d157772 | |||
| cadd466eec | |||
| 3fea0c1e49 | |||
| 4c58201adc | |||
| 4fb4e72624 | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2021.8.4 | ||||
| current_version = 2021.9.8 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem. | ||||
| Output of docker-compose logs or kubectl logs respectively | ||||
|  | ||||
| **Version and Deployment (please complete the following information):** | ||||
|  - authentik version: [e.g. 0.10.0-stable] | ||||
|  - authentik version: [e.g. 2021.8.5] | ||||
|  - Deployment: [e.g. docker-compose, helm] | ||||
|  | ||||
| **Additional context** | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/question.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/question.md
									
									
									
									
										vendored
									
									
								
							| @ -20,7 +20,7 @@ If applicable, add screenshots to help explain your problem. | ||||
| Output of docker-compose logs or kubectl logs respectively | ||||
|  | ||||
| **Version and Deployment (please complete the following information):** | ||||
|  - authentik version: [e.g. 0.10.0-stable] | ||||
|  - authentik version: [e.g. 2021.8.5] | ||||
|  - Deployment: [e.g. docker-compose, helm] | ||||
|  | ||||
| **Additional context** | ||||
|  | ||||
							
								
								
									
										1
									
								
								.github/codespell-words.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.github/codespell-words.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| keypair | ||||
							
								
								
									
										119
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										119
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -2,8 +2,15 @@ name: authentik-ci-main | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|       - next | ||||
|       - version-* | ||||
|     paths-ignore: | ||||
|       - website | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| env: | ||||
|   POSTGRES_DB: authentik | ||||
| @ -18,7 +25,14 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - name: run pylint | ||||
|         run: pipenv run pylint authentik tests lifecycle | ||||
| @ -29,7 +43,14 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - name: run black | ||||
|         run: pipenv run black --check authentik tests lifecycle | ||||
| @ -40,7 +61,14 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - name: run isort | ||||
|         run: pipenv run isort --check authentik tests lifecycle | ||||
| @ -51,7 +79,14 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - name: run bandit | ||||
|         run: pipenv run bandit -r authentik tests lifecycle | ||||
| @ -78,7 +113,14 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - name: run migrations | ||||
|         run: pipenv run python -m lifecycle.migrate | ||||
| @ -86,6 +128,8 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
| @ -94,14 +138,26 @@ jobs: | ||||
|           # Copy current, latest config to local | ||||
|           cp authentik/lib/default.yml local.env.yml | ||||
|           git checkout $(git describe --abbrev=0 --match 'version/*') | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - name: run migrations to stable | ||||
|         run: pipenv run python -m lifecycle.migrate | ||||
|       - name: prepare variables | ||||
|         id: ev | ||||
|         run: | | ||||
|           python ./scripts/gh_do_set_branch.py | ||||
|       - name: checkout current code | ||||
|         run: | | ||||
|           set -x | ||||
|           git checkout $GITHUB_REF | ||||
|           git fetch | ||||
|           git checkout ${{ steps.ev.outputs.branchName }} | ||||
|           pipenv sync --dev | ||||
|       - name: migrate to latest | ||||
|         run: pipenv run python -m lifecycle.migrate | ||||
| @ -112,7 +168,14 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - uses: testspace-com/setup-testspace@v1 | ||||
|         with: | ||||
| @ -124,7 +187,7 @@ jobs: | ||||
|       - name: run testspace | ||||
|         if: ${{ always() }} | ||||
|         run: | | ||||
|           testspace unittest.xml ?add | ||||
|           testspace [unittest]unittest.xml --link=codecov | ||||
|       - if: ${{ always() }} | ||||
|         uses: codecov/codecov-action@v2 | ||||
|   test-integration: | ||||
| @ -134,14 +197,20 @@ jobs: | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: scripts/ci_prepare.sh | ||||
|       - uses: testspace-com/setup-testspace@v1 | ||||
|         with: | ||||
|           domain: ${{github.repository_owner}} | ||||
|       - name: prepare k3d | ||||
|         run: | | ||||
|           wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash | ||||
|       - name: Create k8s Kind Cluster | ||||
|         uses: helm/kind-action@v1.2.0 | ||||
|       - name: run integration | ||||
|         run: | | ||||
|           pipenv run make test-integration | ||||
| @ -149,7 +218,7 @@ jobs: | ||||
|       - name: run testspace | ||||
|         if: ${{ always() }} | ||||
|         run: | | ||||
|           testspace unittest.xml ?add | ||||
|           testspace [integration]unittest.xml --link=codecov | ||||
|       - if: ${{ always() }} | ||||
|         uses: codecov/codecov-action@v2 | ||||
|   test-e2e: | ||||
| @ -167,11 +236,24 @@ jobs: | ||||
|       - uses: testspace-com/setup-testspace@v1 | ||||
|         with: | ||||
|           domain: ${{github.repository_owner}} | ||||
|       # - id: cache-pipenv | ||||
|       #   uses: actions/cache@v2.1.6 | ||||
|       #   with: | ||||
|       #     path: ~/.local/share/virtualenvs | ||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||
|       - name: prepare | ||||
|         # env: | ||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||
|         run: | | ||||
|           scripts/ci_prepare.sh | ||||
|           docker-compose -f tests/e2e/ci.docker-compose.yml up -d | ||||
|       - id: cache-web | ||||
|         uses: actions/cache@v2.1.6 | ||||
|         with: | ||||
|           path: web/dist | ||||
|           key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }} | ||||
|       - name: prepare web ui | ||||
|         if: steps.cache-web.outputs.cache-hit != 'true' | ||||
|         run: | | ||||
|           cd web | ||||
|           npm i | ||||
| @ -183,24 +265,9 @@ jobs: | ||||
|       - name: run testspace | ||||
|         if: ${{ always() }} | ||||
|         run: | | ||||
|           testspace unittest.xml ?add | ||||
|           testspace [e2e]unittest.xml --link=codecov | ||||
|       - if: ${{ always() }} | ||||
|         uses: codecov/codecov-action@v2 | ||||
|   report: | ||||
|     if: ${{ always() }} | ||||
|     needs: | ||||
|       - test-unittest | ||||
|       - test-integration | ||||
|       - test-e2e | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: testspace-com/setup-testspace@v1 | ||||
|         with: | ||||
|           domain: ${{github.repository_owner}} | ||||
|       - name: finish testspace | ||||
|         run: | | ||||
|           testspace ?finish | ||||
|   build: | ||||
|     needs: | ||||
|       - lint-pylint | ||||
| @ -220,11 +287,13 @@ jobs: | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
|       - name: prepare variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }} | ||||
|         run: | | ||||
|           python ./scripts/gh_do_set_branch.py | ||||
|       - name: Login to Container Registry | ||||
|         uses: docker/login-action@v1 | ||||
|         if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} | ||||
|         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|         with: | ||||
|           registry: beryju.org | ||||
|           username: ${{ secrets.HARBOR_USERNAME }} | ||||
| @ -232,9 +301,9 @@ jobs: | ||||
|       - name: Building Docker Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} | ||||
|           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|           tags: | | ||||
|             beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }} | ||||
|             beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }} | ||||
|             beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }} | ||||
|           build-args: | | ||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||
|  | ||||
							
								
								
									
										19
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -2,6 +2,13 @@ name: authentik-ci-outpost | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|       - next | ||||
|       - version-* | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| jobs: | ||||
|   lint-golint: | ||||
| @ -11,9 +18,6 @@ jobs: | ||||
|       - uses: actions/setup-go@v2 | ||||
|         with: | ||||
|           go-version: '^1.16.3' | ||||
|       - name: Generate API | ||||
|         run: | | ||||
|           make gen-outpost | ||||
|       - name: Run linter | ||||
|         run: | | ||||
|           # Create folder structure for go embeds | ||||
| @ -37,17 +41,17 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v1.2.0 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
|       - name: prepare variables | ||||
|         id: ev | ||||
|         env: | ||||
|           DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }} | ||||
|         run: | | ||||
|           python ./scripts/gh_do_set_branch.py | ||||
|       - name: Login to Container Registry | ||||
|         uses: docker/login-action@v1 | ||||
|         if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} | ||||
|         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|         with: | ||||
|           registry: beryju.org | ||||
|           username: ${{ secrets.HARBOR_USERNAME }} | ||||
| @ -55,12 +59,11 @@ jobs: | ||||
|       - name: Building Docker Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} | ||||
|           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||
|           tags: | | ||||
|             beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }} | ||||
|             beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }} | ||||
|             beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }} | ||||
|           file: ${{ matrix.type }}.Dockerfile | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           build-args: | | ||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||
|  | ||||
							
								
								
									
										9
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							| @ -2,6 +2,13 @@ name: authentik-ci-web | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|       - next | ||||
|       - version-* | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| jobs: | ||||
|   lint-eslint: | ||||
| @ -54,7 +61,7 @@ jobs: | ||||
|           npm install | ||||
|       - name: Generate API | ||||
|         run: make gen-web | ||||
|       - name: prettier | ||||
|       - name: lit-analyse | ||||
|         run: | | ||||
|           cd web | ||||
|           npm run lit-analyse | ||||
|  | ||||
							
								
								
									
										24
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -33,14 +33,14 @@ jobs: | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'release' }} | ||||
|           tags: | | ||||
|             beryju/authentik:2021.8.4, | ||||
|             beryju/authentik:2021.9.8, | ||||
|             beryju/authentik:latest, | ||||
|             ghcr.io/goauthentik/server:2021.8.4, | ||||
|             ghcr.io/goauthentik/server:2021.9.8, | ||||
|             ghcr.io/goauthentik/server:latest | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           context: . | ||||
|       - name: Building Docker Image (stable) | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.8.4', 'rc') }} | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.9.8', 'rc') }} | ||||
|         run: | | ||||
|           docker pull beryju/authentik:latest | ||||
|           docker tag beryju/authentik:latest beryju/authentik:stable | ||||
| @ -75,14 +75,14 @@ jobs: | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'release' }} | ||||
|           tags: | | ||||
|             beryju/authentik-proxy:2021.8.4, | ||||
|             beryju/authentik-proxy:2021.9.8, | ||||
|             beryju/authentik-proxy:latest, | ||||
|             ghcr.io/goauthentik/proxy:2021.8.4, | ||||
|             ghcr.io/goauthentik/proxy:2021.9.8, | ||||
|             ghcr.io/goauthentik/proxy:latest | ||||
|           file: proxy.Dockerfile | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|       - name: Building Docker Image (stable) | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.8.4', 'rc') }} | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.9.8', 'rc') }} | ||||
|         run: | | ||||
|           docker pull beryju/authentik-proxy:latest | ||||
|           docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable | ||||
| @ -117,14 +117,14 @@ jobs: | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'release' }} | ||||
|           tags: | | ||||
|             beryju/authentik-ldap:2021.8.4, | ||||
|             beryju/authentik-ldap:2021.9.8, | ||||
|             beryju/authentik-ldap:latest, | ||||
|             ghcr.io/goauthentik/ldap:2021.8.4, | ||||
|             ghcr.io/goauthentik/ldap:2021.9.8, | ||||
|             ghcr.io/goauthentik/ldap:latest | ||||
|           file: ldap.Dockerfile | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|       - name: Building Docker Image (stable) | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.8.4', 'rc') }} | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.9.8', 'rc') }} | ||||
|         run: | | ||||
|           docker pull beryju/authentik-ldap:latest | ||||
|           docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable | ||||
| @ -157,9 +157,9 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Setup Node.js environment | ||||
|         uses: actions/setup-node@v2.4.0 | ||||
|         uses: actions/setup-node@v2 | ||||
|         with: | ||||
|           node-version: 12.x | ||||
|           node-version: '16' | ||||
|       - name: Build web api client and web ui | ||||
|         run: | | ||||
|           export NODE_ENV=production | ||||
| @ -175,7 +175,7 @@ jobs: | ||||
|           SENTRY_PROJECT: authentik | ||||
|           SENTRY_URL: https://sentry.beryju.org | ||||
|         with: | ||||
|           version: authentik@2021.8.4 | ||||
|           version: authentik@2021.9.8 | ||||
|           environment: beryjuorg-prod | ||||
|           sourcemaps: './web/dist' | ||||
|           url_prefix: '~/static/dist' | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -27,7 +27,7 @@ jobs: | ||||
|           docker-compose run -u root server test | ||||
|       - name: Extract version number | ||||
|         id: get_version | ||||
|         uses: actions/github-script@v4.1 | ||||
|         uses: actions/github-script@v5 | ||||
|         with: | ||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           script: | | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -12,7 +12,7 @@ jobs: | ||||
|       # Setup .npmrc file to publish to npm | ||||
|       - uses: actions/setup-node@v2 | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|           node-version: '16' | ||||
|           registry-url: 'https://registry.npmjs.org' | ||||
|       - name: Generate API Client | ||||
|         run: make gen-web | ||||
|  | ||||
| @ -117,7 +117,7 @@ This section guides you through submitting a bug report for authentik. Following | ||||
|  | ||||
| Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form. | ||||
|  | ||||
| This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data. | ||||
| This form will have the full stack trace of the error that occurred and shouldn't contain any sensitive data. | ||||
|  | ||||
| ### Suggesting Enhancements | ||||
|  | ||||
|  | ||||
							
								
								
									
										39
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| # Stage 1: Lock python dependencies | ||||
| FROM python:3.9-slim-buster as locker | ||||
| FROM docker.io/python:3.9-slim-buster as locker | ||||
|  | ||||
| COPY ./Pipfile /app/ | ||||
| COPY ./Pipfile.lock /app/ | ||||
| @ -11,38 +11,23 @@ RUN pip install pipenv && \ | ||||
|     pipenv lock -r --dev-only > requirements-dev.txt | ||||
|  | ||||
| # Stage 2: Build website | ||||
| FROM node as website-builder | ||||
| FROM docker.io/node as website-builder | ||||
|  | ||||
| COPY ./website /static/ | ||||
|  | ||||
| ENV NODE_ENV=production | ||||
| RUN cd /static && npm i && npm run build-docs-only | ||||
|  | ||||
| # Stage 3: Generate API Client | ||||
| FROM openapitools/openapi-generator-cli as go-api-builder | ||||
|  | ||||
| COPY ./schema.yml /local/schema.yml | ||||
|  | ||||
| RUN	docker-entrypoint.sh generate \ | ||||
|     --git-host goauthentik.io \ | ||||
|     --git-repo-id outpost \ | ||||
|     --git-user-id api \ | ||||
|     -i /local/schema.yml \ | ||||
|     -g go \ | ||||
|     -o /local/api \ | ||||
|     --additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true && \ | ||||
|     rm -f /local/api/go.mod /local/api/go.sum | ||||
|  | ||||
| # Stage 4: Build webui | ||||
| FROM node as web-builder | ||||
| # Stage 3: Build webui | ||||
| FROM docker.io/node as web-builder | ||||
|  | ||||
| COPY ./web /static/ | ||||
|  | ||||
| ENV NODE_ENV=production | ||||
| RUN cd /static && npm i && npm run build | ||||
|  | ||||
| # Stage 5: Build go proxy | ||||
| FROM golang:1.17.0 AS builder | ||||
| # Stage 4: Build go proxy | ||||
| FROM docker.io/golang:1.17.1 AS builder | ||||
|  | ||||
| WORKDIR /work | ||||
|  | ||||
| @ -52,7 +37,6 @@ COPY --from=web-builder /static/dist/ /work/web/dist/ | ||||
| COPY --from=web-builder /static/authentik/ /work/web/authentik/ | ||||
| COPY --from=website-builder /static/help/ /work/website/help/ | ||||
|  | ||||
| COPY --from=go-api-builder /local/api api | ||||
| COPY ./cmd /work/cmd | ||||
| COPY ./web/static.go /work/web/static.go | ||||
| COPY ./website/static.go /work/website/static.go | ||||
| @ -62,8 +46,8 @@ COPY ./go.sum /work/go.sum | ||||
|  | ||||
| RUN go build -o /work/authentik ./cmd/server/main.go | ||||
|  | ||||
| # Stage 6: Run | ||||
| FROM python:3.9-slim-buster | ||||
| # Stage 5: Run | ||||
| FROM docker.io/python:3.9-slim-buster | ||||
|  | ||||
| WORKDIR / | ||||
| COPY --from=locker /app/requirements.txt / | ||||
| @ -96,7 +80,12 @@ COPY ./lifecycle/ /lifecycle | ||||
| COPY --from=builder /work/authentik /authentik-proxy | ||||
|  | ||||
| USER authentik | ||||
|  | ||||
| ENV TMPDIR /dev/shm/ | ||||
| ENV PYTHONUBUFFERED 1 | ||||
| ENV PYTHONUNBUFFERED 1 | ||||
| ENV prometheus_multiproc_dir /dev/shm/ | ||||
| ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle" | ||||
|  | ||||
| HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak", "healthcheck" ] | ||||
|  | ||||
| ENTRYPOINT [ "/lifecycle/ak" ] | ||||
|  | ||||
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								Makefile
									
									
									
									
									
								
							| @ -7,8 +7,6 @@ NPM_VERSION = $(shell python -m scripts.npm_version) | ||||
| all: lint-fix lint test gen | ||||
|  | ||||
| test-integration: | ||||
| 	k3d cluster create || exit 0 | ||||
| 	k3d kubeconfig write -o ~/.kube/config --overwrite | ||||
| 	coverage run manage.py test -v 3 tests/integration | ||||
|  | ||||
| test-e2e: | ||||
| @ -22,6 +20,7 @@ test: | ||||
| lint-fix: | ||||
| 	isort authentik tests lifecycle | ||||
| 	black authentik tests lifecycle | ||||
| 	codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w authentik internal cmd web/src website/src | ||||
|  | ||||
| lint: | ||||
| 	pyright authentik tests lifecycle | ||||
| @ -61,13 +60,13 @@ gen-outpost: | ||||
| 		-i /local/schema.yml \ | ||||
| 		-g go \ | ||||
| 		-o /local/api \ | ||||
| 		--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true | ||||
| 		--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false | ||||
| 	rm -f api/go.mod api/go.sum | ||||
|  | ||||
| gen: gen-build gen-clean gen-web gen-outpost | ||||
| gen: gen-build gen-clean gen-web | ||||
|  | ||||
| migrate: | ||||
| 	python -m lifecycle.migrate | ||||
|  | ||||
| run: | ||||
| 	go run -v cmd/server/main.go | ||||
| 	WORKERS=1 go run -v cmd/server/main.go | ||||
|  | ||||
							
								
								
									
										4
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Pipfile
									
									
									
									
									
								
							| @ -48,9 +48,7 @@ duo-client = "*" | ||||
| ua-parser = "*" | ||||
| deepmerge = "*" | ||||
| colorama = "*" | ||||
|  | ||||
| [requires] | ||||
| python_version = "3.9" | ||||
| codespell = "*" | ||||
|  | ||||
| [dev-packages] | ||||
| bandit = "*" | ||||
|  | ||||
							
								
								
									
										429
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										429
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,10 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "f0befa9b3dacc1c3363b9442fa7a43f6be2c46a8fcb80a994230d288a384e54d" | ||||
|             "sha256": "babb6061c555f8f239f00210b2a0356763bdaaca2f3d704cf3444891b84db84d" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": { | ||||
|             "python_version": "3.9" | ||||
|         }, | ||||
|         "requires": {}, | ||||
|         "sources": [ | ||||
|             { | ||||
|                 "name": "pypi", | ||||
| @ -122,27 +120,27 @@ | ||||
|         }, | ||||
|         "boto3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5116e9bdec19adcc5531a9b7b535be77d5314eef092aaf7033ace48a9be65036", | ||||
|                 "sha256:658ddf4ba552f654fd4d48335fa95ff4e3e1a4e82f90021a1a1d3de4a5428ba4" | ||||
|                 "sha256:7b45b224442c479de4bc6e6e9cb0557b642fc7a77edc8702e393ccaa2e0aa128", | ||||
|                 "sha256:c388da7dc1a596755f39de990a72e05cee558d098e81de63de55bd9598cc5134" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.18.34" | ||||
|             "version": "==1.18.48" | ||||
|         }, | ||||
|         "botocore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1b4999fb0e1a4c050c4d9118ebdaac8d83761ef32c3c0f13a25f9204045998fe", | ||||
|                 "sha256:ec2cdf1c8ed64a7f392f352125d248c76103fa9d137b275b7c76836776cedf56" | ||||
|                 "sha256:17a10dd33334e7e3aaa4e12f66317284f96bb53267e20bc877a187c442681772", | ||||
|                 "sha256:2089f9fa36a59d8c02435c49d58ccc7b3ceb9c0c054ea4f71631c3c3a1c5245e" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==1.21.34" | ||||
|             "version": "==1.21.51" | ||||
|         }, | ||||
|         "cachetools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", | ||||
|                 "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" | ||||
|                 "sha256:0a3d3556c2c3befdbba2f93b78792c199c66201c999e97947ea0b7437758246b", | ||||
|                 "sha256:6a6fa6802188ab7e77bab2db001d676e854499552b0037d999d5b9f211db5250" | ||||
|             ], | ||||
|             "markers": "python_version ~= '3.5'", | ||||
|             "version": "==4.2.2" | ||||
|             "version": "==4.2.3" | ||||
|         }, | ||||
|         "cbor2": { | ||||
|             "hashes": [ | ||||
| @ -254,11 +252,11 @@ | ||||
|         }, | ||||
|         "charset-normalizer": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", | ||||
|                 "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" | ||||
|                 "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", | ||||
|                 "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "version": "==2.0.4" | ||||
|             "version": "==2.0.6" | ||||
|         }, | ||||
|         "click": { | ||||
|             "hashes": [ | ||||
| @ -270,9 +268,11 @@ | ||||
|         }, | ||||
|         "click-didyoumean": { | ||||
|             "hashes": [ | ||||
|                 "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb" | ||||
|                 "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", | ||||
|                 "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" | ||||
|             ], | ||||
|             "version": "==0.0.3" | ||||
|             "markers": "python_version < '4' and python_full_version >= '3.6.2'", | ||||
|             "version": "==0.3.0" | ||||
|         }, | ||||
|         "click-plugins": { | ||||
|             "hashes": [ | ||||
| @ -288,6 +288,14 @@ | ||||
|             ], | ||||
|             "version": "==0.2.0" | ||||
|         }, | ||||
|         "codespell": { | ||||
|             "hashes": [ | ||||
|                 "sha256:19d3fe5644fef3425777e66f225a8c82d39059dcfe9edb3349a8a2cf48383ee5", | ||||
|                 "sha256:b864c7d917316316ac24272ee992d7937c3519be4569209c5b60035ac5d569b5" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.1.0" | ||||
|         }, | ||||
|         "colorama": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", | ||||
| @ -305,25 +313,28 @@ | ||||
|         }, | ||||
|         "cryptography": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e", | ||||
|                 "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b", | ||||
|                 "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7", | ||||
|                 "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085", | ||||
|                 "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc", | ||||
|                 "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a", | ||||
|                 "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498", | ||||
|                 "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9", | ||||
|                 "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c", | ||||
|                 "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7", | ||||
|                 "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb", | ||||
|                 "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14", | ||||
|                 "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af", | ||||
|                 "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e", | ||||
|                 "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5", | ||||
|                 "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06", | ||||
|                 "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7" | ||||
|                 "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6", | ||||
|                 "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6", | ||||
|                 "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c", | ||||
|                 "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999", | ||||
|                 "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e", | ||||
|                 "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992", | ||||
|                 "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d", | ||||
|                 "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588", | ||||
|                 "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa", | ||||
|                 "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d", | ||||
|                 "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd", | ||||
|                 "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d", | ||||
|                 "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953", | ||||
|                 "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2", | ||||
|                 "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8", | ||||
|                 "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6", | ||||
|                 "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9", | ||||
|                 "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6", | ||||
|                 "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad", | ||||
|                 "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76" | ||||
|             ], | ||||
|             "version": "==3.4.8" | ||||
|             "version": "==35.0.0" | ||||
|         }, | ||||
|         "dacite": { | ||||
|             "hashes": [ | ||||
| @ -371,11 +382,11 @@ | ||||
|         }, | ||||
|         "django-filter": { | ||||
|             "hashes": [ | ||||
|                 "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06", | ||||
|                 "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1" | ||||
|                 "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e", | ||||
|                 "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.4.0" | ||||
|             "version": "==21.1" | ||||
|         }, | ||||
|         "django-guardian": { | ||||
|             "hashes": [ | ||||
| @ -395,11 +406,11 @@ | ||||
|         }, | ||||
|         "django-otp": { | ||||
|             "hashes": [ | ||||
|                 "sha256:01b5888f0bde5125e139433aacb947e52d5c406fa56c9db43c3e8d75b5c323c4", | ||||
|                 "sha256:0d56dd2a7fbb6ee6e54557e036ca64add0bd3596f471794bad673b7637d5e935" | ||||
|                 "sha256:0c03a471db9e876f3671314bc9a65bd56a5c3c108ee0562c473701310bba4a77", | ||||
|                 "sha256:4c90cdaed683d736b0efafc034a3c6b410e1be2a53c24da287165b1f371d8776" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.0.6" | ||||
|             "version": "==1.1.1" | ||||
|         }, | ||||
|         "django-prometheus": { | ||||
|             "hashes": [ | ||||
| @ -443,19 +454,19 @@ | ||||
|         }, | ||||
|         "docker": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5aafaec0d2a1de0e32010b43b5eac9f6f851c9db99a46ad32b8e44eeeb55616d", | ||||
|                 "sha256:b88eef725b33c0ed59c67506631bbb09b480b7ca5a739bbbb948b446443fe914" | ||||
|                 "sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663", | ||||
|                 "sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==5.0.1" | ||||
|             "version": "==5.0.2" | ||||
|         }, | ||||
|         "drf-spectacular": { | ||||
|             "hashes": [ | ||||
|                 "sha256:98681add6671db9e6dba5f0d3dcf8aab5950cbb978497390507356e593bf082f", | ||||
|                 "sha256:a430bab0f4ecfc90786b7b63bbee3f9a56094201fbed9bdfbf952e99e6469104" | ||||
|                 "sha256:65df818226477cdfa629947ea52bc0cc13eb40550b192eeccec64a6b782651fd", | ||||
|                 "sha256:f71205da3645d770545abeaf48e8a15afd6ee9a76e57c03df4592e51be1059bf" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.18.1" | ||||
|             "version": "==0.19.0" | ||||
|         }, | ||||
|         "duo-client": { | ||||
|             "hashes": [ | ||||
| @ -482,19 +493,19 @@ | ||||
|         }, | ||||
|         "geoip2": { | ||||
|             "hashes": [ | ||||
|                 "sha256:906a1dbf15a179a1af3522970e8420ab15bb3e0afc526942cc179e12146d9c1d", | ||||
|                 "sha256:b97b44031fdc463e84eb1316b4f19edd978cb1d78703465fcb1e36dc5a822ba6" | ||||
|                 "sha256:f150bed3190d543712a17467208388d31bd8ddb49b2226fba53db8aaedb8ba89", | ||||
|                 "sha256:f9172cdfb2a5f9225ace5e30dd7426413ad28798a5f474cd1538780686bd6a87" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==4.2.0" | ||||
|             "version": "==4.4.0" | ||||
|         }, | ||||
|         "google-auth": { | ||||
|             "hashes": [ | ||||
|                 "sha256:104475dc4d57bbae49017aea16fffbb763204fa2d6a70f1f3cc79962c1a383a4", | ||||
|                 "sha256:cde472372e030e1e0bc64dac00fb53e6c095d7ab641f4281e2c995e85e205d8b" | ||||
|                 "sha256:2a92b485afed5292946b324e91fcbe03db277ee4cb64c998c6cfa66d4af01dee", | ||||
|                 "sha256:6dc8173abd50f25b6e62fc5b42802c96fc7cd9deb9bfeeb10a79f5606225cdf4" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==2.0.2" | ||||
|             "version": "==2.2.1" | ||||
|         }, | ||||
|         "gunicorn": { | ||||
|             "hashes": [ | ||||
| @ -618,10 +629,10 @@ | ||||
|         }, | ||||
|         "jsonschema": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", | ||||
|                 "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" | ||||
|                 "sha256:bc51325b929171791c42ebc1c70b9713eb134d3bb8ebd5474c8b659b15be6d86", | ||||
|                 "sha256:c773028c649441ab980015b5b622f4cd5134cf563daaf0235ca4b73cc3734f20" | ||||
|             ], | ||||
|             "version": "==3.2.0" | ||||
|             "version": "==4.0.0" | ||||
|         }, | ||||
|         "kombu": { | ||||
|             "hashes": [ | ||||
| @ -706,10 +717,10 @@ | ||||
|         }, | ||||
|         "maxminddb": { | ||||
|             "hashes": [ | ||||
|                 "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" | ||||
|                 "sha256:e37707ec4fab115804670e0fb7aedb4b57075a8b6f80052bdc648d3c005184e5" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==2.0.3" | ||||
|             "version": "==2.2.0" | ||||
|         }, | ||||
|         "msgpack": { | ||||
|             "hashes": [ | ||||
| @ -900,39 +911,39 @@ | ||||
|         }, | ||||
|         "pycryptodome": { | ||||
|             "hashes": [ | ||||
|                 "sha256:09c1555a3fa450e7eaca41ea11cd00afe7c91fef52353488e65663777d8524e0", | ||||
|                 "sha256:12222a5edc9ca4a29de15fbd5339099c4c26c56e13c2ceddf0b920794f26165d", | ||||
|                 "sha256:1723ebee5561628ce96748501cdaa7afaa67329d753933296321f0be55358dce", | ||||
|                 "sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06", | ||||
|                 "sha256:2603c98ae04aac675fefcf71a6c87dc4bb74a75e9071ae3923bbc91a59f08d35", | ||||
|                 "sha256:2dea65df54349cdfa43d6b2e8edb83f5f8d6861e5cf7b1fbc3e34c5694c85e27", | ||||
|                 "sha256:31c1df17b3dc5f39600a4057d7db53ac372f492c955b9b75dd439f5d8b460129", | ||||
|                 "sha256:38661348ecb71476037f1e1f553159b80d256c00f6c0b00502acac891f7116d9", | ||||
|                 "sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673", | ||||
|                 "sha256:3f840c49d38986f6e17dbc0673d37947c88bc9d2d9dba1c01b979b36f8447db1", | ||||
|                 "sha256:501ab36aae360e31d0ec370cf5ce8ace6cb4112060d099b993bc02b36ac83fb6", | ||||
|                 "sha256:60386d1d4cfaad299803b45a5bc2089696eaf6cdd56f9fc17479a6f89595cfc8", | ||||
|                 "sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c", | ||||
|                 "sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713", | ||||
|                 "sha256:6d2df5223b12437e644ce0a3be7809471ffa71de44ccd28b02180401982594a6", | ||||
|                 "sha256:758949ca62690b1540dfb24ad773c6da9cd0e425189e83e39c038bbd52b8e438", | ||||
|                 "sha256:77997519d8eb8a4adcd9a47b9cec18f9b323e296986528186c0e9a7a15d6a07e", | ||||
|                 "sha256:7fd519b89585abf57bf47d90166903ec7b43af4fe23c92273ea09e6336af5c07", | ||||
|                 "sha256:98213ac2b18dc1969a47bc65a79a8fca02a414249d0c8635abb081c7f38c91b6", | ||||
|                 "sha256:99b2f3fc51d308286071d0953f92055504a6ffe829a832a9fc7a04318a7683dd", | ||||
|                 "sha256:9b6f711b25e01931f1c61ce0115245a23cdc8b80bf8539ac0363bdcf27d649b6", | ||||
|                 "sha256:a3105a0eb63eacf98c2ecb0eb4aa03f77f40fbac2bdde22020bb8a536b226bb8", | ||||
|                 "sha256:a8eb8b6ea09ec1c2535bf39914377bc8abcab2c7d30fa9225eb4fe412024e427", | ||||
|                 "sha256:a92d5c414e8ee1249e850789052608f582416e82422502dc0ac8c577808a9067", | ||||
|                 "sha256:d3d6958d53ad307df5e8469cc44474a75393a434addf20ecd451f38a72fe29b8", | ||||
|                 "sha256:e0a4d5933a88a2c98bbe19c0c722f5483dc628d7a38338ac2cb64a7dbd34064b", | ||||
|                 "sha256:e3bf558c6aeb49afa9f0c06cee7fb5947ee5a1ff3bd794b653d39926b49077fa", | ||||
|                 "sha256:e61e363d9a5d7916f3a4ce984a929514c0df3daf3b1b2eb5e6edbb131ee771cf", | ||||
|                 "sha256:f977cdf725b20f6b8229b0c87acb98c7717e742ef9f46b113985303ae12a99da", | ||||
|                 "sha256:fc7489a50323a0df02378bc2fff86eb69d94cc5639914346c736be981c6a02e7" | ||||
|                 "sha256:04e14c732c3693d2830839feed5129286ce47ffa8bfe90e4ae042c773e51c677", | ||||
|                 "sha256:11d3164fb49fdee000fde05baecce103c0c698168ef1a18d9c7429dd66f0f5bb", | ||||
|                 "sha256:217dcc0c92503f7dd4b3d3b7d974331a4419f97f555c99a845c3b366fed7056b", | ||||
|                 "sha256:24c1b7705d19d8ae3e7255431efd2e526006855df62620118dd7b5374c6372f6", | ||||
|                 "sha256:309529d2526f3fb47102aeef376b3459110a6af7efb162e860b32e3a17a46f06", | ||||
|                 "sha256:3a153658d97258ca20bf18f7fe31c09cc7c558b6f8974a6ec74e19f6c634bd64", | ||||
|                 "sha256:3f9fb499e267039262569d08658132c9cd8b136bf1d8c56b72f70ed05551e526", | ||||
|                 "sha256:3faa6ebd35c61718f3f8862569c1f38450c24f3ededb213e1a64806f02f584bc", | ||||
|                 "sha256:40083b0d7f277452c7f2dd4841801f058cc12a74c219ee4110d65774c6a58bef", | ||||
|                 "sha256:49e54f2245befb0193848c8c8031d8d1358ed4af5a1ae8d0a3ba669a5cdd3a72", | ||||
|                 "sha256:4e8fc4c48365ce8a542fe48bf1360da05bb2851df12f64fc94d751705e7cdbe7", | ||||
|                 "sha256:54d4e4d45f349d8c4e2f31c2734637ff62a844af391b833f789da88e43a8f338", | ||||
|                 "sha256:66301e4c42dee43ee2da256625d3fe81ef98cc9924c2bd535008cc3ad8ded77b", | ||||
|                 "sha256:6b45fcace5a5d9c57ba87cf804b161adc62aa826295ce7f7acbcbdc0df74ed37", | ||||
|                 "sha256:7efec2418e9746ec48e264eea431f8e422d931f71c57b1c96ee202b117f58fa9", | ||||
|                 "sha256:851e6d4930b160417235955322db44adbdb19589918670d63f4acd5d92959ac0", | ||||
|                 "sha256:8e82524e7c354033508891405574d12e612cc4fdd3b55d2c238fc1a3e300b606", | ||||
|                 "sha256:8ec154ec445412df31acf0096e7f715e30e167c8f2318b8f5b1ab7c28f4c82f7", | ||||
|                 "sha256:91ba4215a1f37d0f371fe43bc88c5ff49c274849f3868321c889313787de7672", | ||||
|                 "sha256:97e7df67a4da2e3f60612bbfd6c3f243a63a15d8f4797dd275e1d7b44a65cb12", | ||||
|                 "sha256:9a2312440057bf29b9582f72f14d79692044e63bfbc4b4bbea8559355f44f3dd", | ||||
|                 "sha256:a7471646d8cd1a58bb696d667dcb3853e5c9b341b68dcf3c3cc0893d0f98ca5f", | ||||
|                 "sha256:ac3012c36633564b2b5539bb7c6d9175f31d2ce74844e9abe654c428f02d0fd8", | ||||
|                 "sha256:b1daf251395af7336ddde6a0015ba5e632c18fe646ba930ef87402537358e3b4", | ||||
|                 "sha256:b217b4525e60e1af552d62bec01b4685095436d4de5ecde0f05d75b2f95ba6d4", | ||||
|                 "sha256:c61ea053bd5d4c12a063d7e704fbe1c45abb5d2510dab55bd95d166ba661604f", | ||||
|                 "sha256:c6469d1453f5864e3321a172b0aa671b938d753cbf2376b99fa2ab8841539bb8", | ||||
|                 "sha256:cefe6b267b8e5c3c72e11adec35a9c7285b62e8ea141b63e87055e9a9e5f2f8c", | ||||
|                 "sha256:d713dc0910e5ded07852a05e9b75f1dd9d3a31895eebee0668f612779b2a748c", | ||||
|                 "sha256:db15fa07d2a4c00beeb5e9acdfdbc1c79f9ccfbdc1a8f36c82c4aa44951b33c9" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.10.1" | ||||
|             "version": "==3.10.4" | ||||
|         }, | ||||
|         "pyjwt": { | ||||
|             "hashes": [ | ||||
| @ -944,10 +955,10 @@ | ||||
|         }, | ||||
|         "pyopenssl": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51", | ||||
|                 "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b" | ||||
|                 "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3", | ||||
|                 "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6" | ||||
|             ], | ||||
|             "version": "==20.0.1" | ||||
|             "version": "==21.0.0" | ||||
|         }, | ||||
|         "pyparsing": { | ||||
|             "hashes": [ | ||||
| @ -1084,11 +1095,11 @@ | ||||
|         }, | ||||
|         "sentry-sdk": { | ||||
|             "hashes": [ | ||||
|                 "sha256:ebe99144fa9618d4b0e7617e7929b75acd905d258c3c779edcd34c0adfffe26c", | ||||
|                 "sha256:f33d34c886d0ba24c75ea8885a8b3a172358853c7cbde05979fc99c29ef7bc52" | ||||
|                 "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828", | ||||
|                 "sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.3.1" | ||||
|             "version": "==1.4.3" | ||||
|         }, | ||||
|         "service-identity": { | ||||
|             "hashes": [ | ||||
| @ -1108,11 +1119,11 @@ | ||||
|         }, | ||||
|         "sqlparse": { | ||||
|             "hashes": [ | ||||
|                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", | ||||
|                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" | ||||
|                 "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", | ||||
|                 "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==0.4.1" | ||||
|             "version": "==0.4.2" | ||||
|         }, | ||||
|         "structlog": { | ||||
|             "hashes": [ | ||||
| @ -1178,11 +1189,11 @@ | ||||
|                 "secure" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", | ||||
|                 "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" | ||||
|                 "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", | ||||
|                 "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.26.6" | ||||
|             "version": "==1.26.7" | ||||
|         }, | ||||
|         "uvicorn": { | ||||
|             "extras": [ | ||||
| @ -1256,58 +1267,50 @@ | ||||
|         }, | ||||
|         "websockets": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc", | ||||
|                 "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e", | ||||
|                 "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135", | ||||
|                 "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02", | ||||
|                 "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3", | ||||
|                 "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf", | ||||
|                 "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b", | ||||
|                 "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2", | ||||
|                 "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af", | ||||
|                 "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d", | ||||
|                 "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880", | ||||
|                 "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077", | ||||
|                 "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f", | ||||
|                 "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec", | ||||
|                 "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25", | ||||
|                 "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0", | ||||
|                 "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe", | ||||
|                 "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a", | ||||
|                 "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb", | ||||
|                 "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d", | ||||
|                 "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857", | ||||
|                 "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c", | ||||
|                 "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0", | ||||
|                 "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40", | ||||
|                 "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4", | ||||
|                 "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20", | ||||
|                 "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314", | ||||
|                 "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da", | ||||
|                 "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58", | ||||
|                 "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2", | ||||
|                 "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd", | ||||
|                 "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a", | ||||
|                 "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd" | ||||
|                 "sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8", | ||||
|                 "sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b", | ||||
|                 "sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539", | ||||
|                 "sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939", | ||||
|                 "sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4", | ||||
|                 "sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80", | ||||
|                 "sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474", | ||||
|                 "sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76", | ||||
|                 "sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a", | ||||
|                 "sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37", | ||||
|                 "sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238", | ||||
|                 "sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379", | ||||
|                 "sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805", | ||||
|                 "sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7", | ||||
|                 "sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537", | ||||
|                 "sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456", | ||||
|                 "sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c", | ||||
|                 "sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002", | ||||
|                 "sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567", | ||||
|                 "sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da", | ||||
|                 "sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a", | ||||
|                 "sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368", | ||||
|                 "sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2", | ||||
|                 "sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1", | ||||
|                 "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465" | ||||
|             ], | ||||
|             "version": "==9.1" | ||||
|             "version": "==10.0" | ||||
|         }, | ||||
|         "xmlsec": { | ||||
|             "hashes": [ | ||||
|                 "sha256:23f209260b37bdc2fd96af837494c47dd1e67964f077442b63acd83c0f62e212", | ||||
|                 "sha256:4fb38ab0bf3e47cbae136119674a869e09d61c939b510350f369c8ac46087373", | ||||
|                 "sha256:705ab5b848afdf3a5c78b1322276054c885f44dc51601e14cb883a9c86cbe20f", | ||||
|                 "sha256:843d10bba4c480609da74ee11fff1ee0fc1c12821c656979f12a7a4ecb043e03", | ||||
|                 "sha256:86d54b93f8278e2f0c504d0744e39a483c1c7ce9993f2ca70184cc7770faa982", | ||||
|                 "sha256:8922fba55a060ee81de4a7f5efc593c5bf121047763aecf0eead02e061c9d2db", | ||||
|                 "sha256:c7b49d4fce83186b89f7ce6cec765245d36a70d0acc2f3ed0ba95c735b3667da", | ||||
|                 "sha256:cd2eaaff7f31784a07dd99ce81fa767313df3ba1834faa4143ee2c07000cac7a", | ||||
|                 "sha256:dea5bef9b5830c36ccb7a68a0d94d49eaea4d03fbbd04179652bf661b7e6e30f", | ||||
|                 "sha256:eadff662d89c80db409c69d82eb3e695e16d4a5e8ab56b5b22670a54e9c6ff20", | ||||
|                 "sha256:ee233d0bc27fb8f447ca2622b0de2ac2df45b8795f02ef263825912011fe4fe9" | ||||
|                 "sha256:135724cdce60e6bbd072fca6f09a21f72e2cecc59eebb4eed7740c316ecabc7b", | ||||
|                 "sha256:1b4377f6d37ad714ba95a227ef40fb54ba1b22ef5170ce04c330fe45ee6ad184", | ||||
|                 "sha256:2c86ac6ce570c9e04f04da0cd5e7d3db346e4b5b1d006311606368f17c756ef9", | ||||
|                 "sha256:4e5f565de311afa33aaee4724566e685f951afe301212b6cf82f98cf9d8a1749", | ||||
|                 "sha256:9a2b8a780093b0fe8cecae53a81a8cd9edd50c08980d374c5317c91f065042d9", | ||||
|                 "sha256:ce9c681adbc87b4f06c2b16725d9b2edbdbd508117dae4288b5faf78c1406038", | ||||
|                 "sha256:d22da4d3dcc559fb2e54e782f39c9ddad5f8d5b356f86a79bbb80b0a45115c97", | ||||
|                 "sha256:db3e18ca883c01bbe28c9f5197c66f676c9772cf2d85f667e6122fc4d0702225", | ||||
|                 "sha256:e4783f7814aa2a3e318385cce8ef87c82954b9a59535a48f67da4e2c21c08ce1", | ||||
|                 "sha256:f32e54065f0404ceff71388daa7fa7df10e1fb800051dfe302d63abb0acf0020", | ||||
|                 "sha256:f5d242b1a19a36078608f5d7f4d561c5ca55cac8061a323a071c06275267dc19" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.3.11" | ||||
|             "version": "==1.3.12" | ||||
|         }, | ||||
|         "yarl": { | ||||
|             "hashes": [ | ||||
| @ -1420,11 +1423,11 @@ | ||||
|         }, | ||||
|         "astroid": { | ||||
|             "hashes": [ | ||||
|                 "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c", | ||||
|                 "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e" | ||||
|                 "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471", | ||||
|                 "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708" | ||||
|             ], | ||||
|             "markers": "python_version ~= '3.6'", | ||||
|             "version": "==2.7.3" | ||||
|             "version": "==2.8.0" | ||||
|         }, | ||||
|         "attrs": { | ||||
|             "hashes": [ | ||||
| @ -1467,11 +1470,11 @@ | ||||
|         }, | ||||
|         "charset-normalizer": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", | ||||
|                 "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" | ||||
|                 "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", | ||||
|                 "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" | ||||
|             ], | ||||
|             "markers": "python_version >= '3'", | ||||
|             "version": "==2.0.4" | ||||
|             "version": "==2.0.6" | ||||
|         }, | ||||
|         "click": { | ||||
|             "hashes": [ | ||||
| @ -1557,11 +1560,11 @@ | ||||
|         }, | ||||
|         "gitpython": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b", | ||||
|                 "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8" | ||||
|                 "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647", | ||||
|                 "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==3.1.18" | ||||
|             "markers": "python_version >= '3.7'", | ||||
|             "version": "==3.1.24" | ||||
|         }, | ||||
|         "idna": { | ||||
|             "hashes": [ | ||||
| @ -1652,11 +1655,11 @@ | ||||
|         }, | ||||
|         "platformdirs": { | ||||
|             "hashes": [ | ||||
|                 "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f", | ||||
|                 "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648" | ||||
|                 "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", | ||||
|                 "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==2.3.0" | ||||
|             "version": "==2.4.0" | ||||
|         }, | ||||
|         "pluggy": { | ||||
|             "hashes": [ | ||||
| @ -1676,11 +1679,11 @@ | ||||
|         }, | ||||
|         "pylint": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1", | ||||
|                 "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852" | ||||
|                 "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126", | ||||
|                 "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.10.2" | ||||
|             "version": "==2.11.1" | ||||
|         }, | ||||
|         "pylint-django": { | ||||
|             "hashes": [ | ||||
| @ -1758,49 +1761,49 @@ | ||||
|         }, | ||||
|         "regex": { | ||||
|             "hashes": [ | ||||
|                 "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468", | ||||
|                 "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", | ||||
|                 "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", | ||||
|                 "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", | ||||
|                 "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc", | ||||
|                 "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8", | ||||
|                 "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797", | ||||
|                 "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2", | ||||
|                 "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13", | ||||
|                 "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d", | ||||
|                 "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a", | ||||
|                 "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0", | ||||
|                 "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73", | ||||
|                 "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1", | ||||
|                 "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed", | ||||
|                 "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a", | ||||
|                 "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b", | ||||
|                 "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", | ||||
|                 "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", | ||||
|                 "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", | ||||
|                 "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2", | ||||
|                 "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", | ||||
|                 "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", | ||||
|                 "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", | ||||
|                 "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", | ||||
|                 "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a", | ||||
|                 "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", | ||||
|                 "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", | ||||
|                 "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c", | ||||
|                 "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", | ||||
|                 "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", | ||||
|                 "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", | ||||
|                 "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e", | ||||
|                 "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", | ||||
|                 "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", | ||||
|                 "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", | ||||
|                 "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd", | ||||
|                 "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", | ||||
|                 "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", | ||||
|                 "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", | ||||
|                 "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759" | ||||
|                 "sha256:0628ed7d6334e8f896f882a5c1240de8c4d9b0dd7c7fb8e9f4692f5684b7d656", | ||||
|                 "sha256:09eb62654030f39f3ba46bc6726bea464069c29d00a9709e28c9ee9623a8da4a", | ||||
|                 "sha256:0bba1f6df4eafe79db2ecf38835c2626dbd47911e0516f6962c806f83e7a99ae", | ||||
|                 "sha256:10a7a9cbe30bd90b7d9a1b4749ef20e13a3528e4215a2852be35784b6bd070f0", | ||||
|                 "sha256:17310b181902e0bb42b29c700e2c2346b8d81f26e900b1328f642e225c88bce1", | ||||
|                 "sha256:1e8d1898d4fb817120a5f684363b30108d7b0b46c7261264b100d14ec90a70e7", | ||||
|                 "sha256:2054dea683f1bda3a804fcfdb0c1c74821acb968093d0be16233873190d459e3", | ||||
|                 "sha256:29385c4dbb3f8b3a55ce13de6a97a3d21bd00de66acd7cdfc0b49cb2f08c906c", | ||||
|                 "sha256:295bc8a13554a25ad31e44c4bedabd3c3e28bba027e4feeb9bb157647a2344a7", | ||||
|                 "sha256:2cdb3789736f91d0b3333ac54d12a7e4f9efbc98f53cb905d3496259a893a8b3", | ||||
|                 "sha256:3baf3eaa41044d4ced2463fd5d23bf7bd4b03d68739c6c99a59ce1f95599a673", | ||||
|                 "sha256:4e61100200fa6ab7c99b61476f9f9653962ae71b931391d0264acfb4d9527d9c", | ||||
|                 "sha256:6266fde576e12357b25096351aac2b4b880b0066263e7bc7a9a1b4307991bb0e", | ||||
|                 "sha256:650c4f1fc4273f4e783e1d8e8b51a3e2311c2488ba0fcae6425b1e2c248a189d", | ||||
|                 "sha256:658e3477676009083422042c4bac2bdad77b696e932a3de001c42cc046f8eda2", | ||||
|                 "sha256:6adc1bd68f81968c9d249aab8c09cdc2cbe384bf2d2cb7f190f56875000cdc72", | ||||
|                 "sha256:6c4d83d21d23dd854ffbc8154cf293f4e43ba630aa9bd2539c899343d7f59da3", | ||||
|                 "sha256:6f74b6d8f59f3cfb8237e25c532b11f794b96f5c89a6f4a25857d85f84fbef11", | ||||
|                 "sha256:7783d89bd5413d183a38761fbc68279b984b9afcfbb39fa89d91f63763fbfb90", | ||||
|                 "sha256:7e3536f305f42ad6d31fc86636c54c7dafce8d634e56fef790fbacb59d499dd5", | ||||
|                 "sha256:821e10b73e0898544807a0692a276e539e5bafe0a055506a6882814b6a02c3ec", | ||||
|                 "sha256:835962f432bce92dc9bf22903d46c50003c8d11b1dc64084c8fae63bca98564a", | ||||
|                 "sha256:85c61bee5957e2d7be390392feac7e1d7abd3a49cbaed0c8cee1541b784c8561", | ||||
|                 "sha256:86f9931eb92e521809d4b64ec8514f18faa8e11e97d6c2d1afa1bcf6c20a8eab", | ||||
|                 "sha256:8a5c2250c0a74428fd5507ae8853706fdde0f23bfb62ee1ec9418eeacf216078", | ||||
|                 "sha256:8aec4b4da165c4a64ea80443c16e49e3b15df0f56c124ac5f2f8708a65a0eddc", | ||||
|                 "sha256:8c268e78d175798cd71d29114b0a1f1391c7d011995267d3b62319ec1a4ecaa1", | ||||
|                 "sha256:8d80087320632457aefc73f686f66139801959bf5b066b4419b92be85be3543c", | ||||
|                 "sha256:95e89a8558c8c48626dcffdf9c8abac26b7c251d352688e7ab9baf351e1c7da6", | ||||
|                 "sha256:9c371dd326289d85906c27ec2bc1dcdedd9d0be12b543d16e37bad35754bde48", | ||||
|                 "sha256:9c7cb25adba814d5f419733fe565f3289d6fa629ab9e0b78f6dff5fa94ab0456", | ||||
|                 "sha256:a731552729ee8ae9c546fb1c651c97bf5f759018fdd40d0e9b4d129e1e3a44c8", | ||||
|                 "sha256:aea4006b73b555fc5bdb650a8b92cf486d678afa168cf9b38402bb60bf0f9c18", | ||||
|                 "sha256:b0e3f59d3c772f2c3baaef2db425e6fc4149d35a052d874bb95ccfca10a1b9f4", | ||||
|                 "sha256:b15dc34273aefe522df25096d5d087abc626e388a28a28ac75a4404bb7668736", | ||||
|                 "sha256:c000635fd78400a558bd7a3c2981bb2a430005ebaa909d31e6e300719739a949", | ||||
|                 "sha256:c31f35a984caffb75f00a86852951a337540b44e4a22171354fb760cefa09346", | ||||
|                 "sha256:c50a6379763c733562b1fee877372234d271e5c78cd13ade5f25978aa06744db", | ||||
|                 "sha256:c94722bf403b8da744b7d0bb87e1f2529383003ceec92e754f768ef9323f69ad", | ||||
|                 "sha256:dcbbc9cfa147d55a577d285fd479b43103188855074552708df7acc31a476dd9", | ||||
|                 "sha256:fb9f5844db480e2ef9fce3a72e71122dd010ab7b2920f777966ba25f7eb63819" | ||||
|             ], | ||||
|             "version": "==2021.8.28" | ||||
|             "version": "==2021.9.24" | ||||
|         }, | ||||
|         "requests": { | ||||
|             "hashes": [ | ||||
| @ -1858,16 +1861,24 @@ | ||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||
|             "version": "==0.10.2" | ||||
|         }, | ||||
|         "typing-extensions": { | ||||
|             "hashes": [ | ||||
|                 "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", | ||||
|                 "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", | ||||
|                 "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" | ||||
|             ], | ||||
|             "version": "==3.10.0.2" | ||||
|         }, | ||||
|         "urllib3": { | ||||
|             "extras": [ | ||||
|                 "secure" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", | ||||
|                 "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" | ||||
|                 "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", | ||||
|                 "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.26.6" | ||||
|             "version": "==1.26.7" | ||||
|         }, | ||||
|         "wrapt": { | ||||
|             "hashes": [ | ||||
|  | ||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @ -4,14 +4,14 @@ | ||||
|  | ||||
| --- | ||||
|  | ||||
| [](https://discord.gg/jg33eMhnj6) | ||||
|  | ||||
|  | ||||
|  | ||||
| [](https://discord.gg/jg33eMhnj6) | ||||
| [](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml) | ||||
| [](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml) | ||||
| [](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml) | ||||
| [](https://codecov.io/gh/goauthentik/authentik) | ||||
| [](https://goauthentik.testspace.com/) | ||||
|  | ||||
|  | ||||
|  | ||||
| [](https://www.transifex.com/beryjuorg/authentik/) | ||||
|  | ||||
| ## What is authentik? | ||||
|  | ||||
| @ -6,9 +6,8 @@ | ||||
|  | ||||
| | Version    | Supported          | | ||||
| | ---------- | ------------------ | | ||||
| | 2021.5.x   | :white_check_mark: | | ||||
| | 2021.6.x   | :white_check_mark: | | ||||
| | 2021.7.x   | :white_check_mark: | | ||||
| | 2021.8.x   | :white_check_mark: | | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| """authentik""" | ||||
| __version__ = "2021.8.4" | ||||
| __version__ = "2021.9.8" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
| @ -84,7 +84,7 @@ class SystemSerializer(PassiveSerializer): | ||||
|         return now() | ||||
|  | ||||
|     def get_embedded_outpost_host(self, request: Request) -> str: | ||||
|         """Get the FQDN configured on the embeddded outpost""" | ||||
|         """Get the FQDN configured on the embedded outpost""" | ||||
|         outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) | ||||
|         if not outposts.exists(): | ||||
|             return "" | ||||
|  | ||||
| @ -8,3 +8,8 @@ class AuthentikAdminConfig(AppConfig): | ||||
|     name = "authentik.admin" | ||||
|     label = "authentik_admin" | ||||
|     verbose_name = "authentik Admin" | ||||
|  | ||||
|     def ready(self): | ||||
|         from authentik.admin.tasks import clear_update_notifications | ||||
|  | ||||
|         clear_update_notifications.delay() | ||||
|  | ||||
| @ -6,12 +6,14 @@ from django.core.cache import cache | ||||
| from django.core.validators import URLValidator | ||||
| from packaging.version import parse | ||||
| from prometheus_client import Info | ||||
| from requests import RequestException, get | ||||
| from requests import RequestException | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.models import Event, EventAction, Notification | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.http import get_http_session | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| @ -33,15 +35,32 @@ def _set_prom_info(): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def clear_update_notifications(): | ||||
|     """Clear update notifications on startup if the notification was for the version | ||||
|     we're running now.""" | ||||
|     for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE): | ||||
|         if "new_version" not in notification.event.context: | ||||
|             continue | ||||
|         notification_version = notification.event.context["new_version"] | ||||
|         if notification_version == __version__: | ||||
|             notification.delete() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| def update_latest_version(self: MonitoredTask): | ||||
|     """Update latest version info""" | ||||
|     if CONFIG.y_bool("disable_update_check"): | ||||
|         cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status(TaskResult(TaskResultStatus.WARNING, messages=["Version check disabled."])) | ||||
|         return | ||||
|     try: | ||||
|         response = get("https://api.github.com/repos/goauthentik/authentik/releases/latest") | ||||
|         response = get_http_session().get( | ||||
|             "https://version.goauthentik.io/version.json", | ||||
|         ) | ||||
|         response.raise_for_status() | ||||
|         data = response.json() | ||||
|         tag_name = data.get("tag_name") | ||||
|         upstream_version = tag_name.split("/")[1] | ||||
|         upstream_version = data.get("stable", {}).get("version") | ||||
|         cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status( | ||||
|             TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]) | ||||
| @ -58,7 +77,7 @@ def update_latest_version(self: MonitoredTask): | ||||
|             ).exists(): | ||||
|                 return | ||||
|             event_dict = {"new_version": upstream_version} | ||||
|             if match := re.search(URL_FINDER, data.get("body", "")): | ||||
|             if match := re.search(URL_FINDER, data.get("stable", {}).get("changelog", "")): | ||||
|                 event_dict["message"] = f"Changelog: {match.group()}" | ||||
|             Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save() | ||||
|     except (RequestException, IndexError) as exc: | ||||
|  | ||||
| @ -1,81 +1,58 @@ | ||||
| """test admin tasks""" | ||||
| import json | ||||
| from dataclasses import dataclass | ||||
| from unittest.mock import Mock, patch | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.test import TestCase | ||||
| from requests.exceptions import RequestException | ||||
| from requests_mock import Mocker | ||||
|  | ||||
| from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class MockResponse: | ||||
|     """Mock class to emulate the methods of requests's Response we need""" | ||||
|  | ||||
|     status_code: int | ||||
|     response: str | ||||
|  | ||||
|     def json(self) -> dict: | ||||
|         """Get json parsed response""" | ||||
|         return json.loads(self.response) | ||||
|  | ||||
|     def raise_for_status(self): | ||||
|         """raise RequestException if status code is 400 or more""" | ||||
|         if self.status_code >= 400: | ||||
|             raise RequestException | ||||
|  | ||||
|  | ||||
| REQUEST_MOCK_VALID = Mock( | ||||
|     return_value=MockResponse( | ||||
|         200, | ||||
|         """{ | ||||
|             "tag_name": "version/99999999.9999999", | ||||
|             "body": "https://goauthentik.io/test" | ||||
|         }""", | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}")) | ||||
| RESPONSE_VALID = { | ||||
|     "$schema": "https://version.goauthentik.io/schema.json", | ||||
|     "stable": { | ||||
|         "version": "99999999.9999999", | ||||
|         "changelog": "See https://goauthentik.io/test", | ||||
|         "reason": "bugfix", | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| class TestAdminTasks(TestCase): | ||||
|     """test admin tasks""" | ||||
|  | ||||
|     @patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID) | ||||
|     def test_version_valid_response(self): | ||||
|         """Test Update checker with valid response""" | ||||
|         update_latest_version.delay().get() | ||||
|         self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999") | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.UPDATE_AVAILABLE, | ||||
|                 context__new_version="99999999.9999999", | ||||
|                 context__message="Changelog: https://goauthentik.io/test", | ||||
|             ).exists() | ||||
|         ) | ||||
|         # test that a consecutive check doesn't create a duplicate event | ||||
|         update_latest_version.delay().get() | ||||
|         self.assertEqual( | ||||
|             len( | ||||
|         with Mocker() as mocker: | ||||
|             mocker.get("https://version.goauthentik.io/version.json", json=RESPONSE_VALID) | ||||
|             update_latest_version.delay().get() | ||||
|             self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999") | ||||
|             self.assertTrue( | ||||
|                 Event.objects.filter( | ||||
|                     action=EventAction.UPDATE_AVAILABLE, | ||||
|                     context__new_version="99999999.9999999", | ||||
|                     context__message="Changelog: https://goauthentik.io/test", | ||||
|                 ) | ||||
|             ), | ||||
|             1, | ||||
|         ) | ||||
|                 ).exists() | ||||
|             ) | ||||
|             # test that a consecutive check doesn't create a duplicate event | ||||
|             update_latest_version.delay().get() | ||||
|             self.assertEqual( | ||||
|                 len( | ||||
|                     Event.objects.filter( | ||||
|                         action=EventAction.UPDATE_AVAILABLE, | ||||
|                         context__new_version="99999999.9999999", | ||||
|                         context__message="Changelog: https://goauthentik.io/test", | ||||
|                     ) | ||||
|                 ), | ||||
|                 1, | ||||
|             ) | ||||
|  | ||||
|     @patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID) | ||||
|     def test_version_error(self): | ||||
|         """Test Update checker with invalid response""" | ||||
|         update_latest_version.delay().get() | ||||
|         self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0") | ||||
|         self.assertFalse( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" | ||||
|             ).exists() | ||||
|         ) | ||||
|         with Mocker() as mocker: | ||||
|             mocker.get("https://version.goauthentik.io/version.json", status_code=400) | ||||
|             update_latest_version.delay().get() | ||||
|             self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0") | ||||
|             self.assertFalse( | ||||
|                 Event.objects.filter( | ||||
|                     action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" | ||||
|                 ).exists() | ||||
|             ) | ||||
|  | ||||
| @ -40,7 +40,6 @@ def bearer_auth(raw_header: bytes) -> Optional[User]: | ||||
|         raise AuthenticationFailed("Malformed header") | ||||
|     tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API) | ||||
|     if not tokens.exists(): | ||||
|         LOGGER.info("Authenticating via secret_key") | ||||
|         user = token_secret_key(password) | ||||
|         if not user: | ||||
|             raise AuthenticationFailed("Token invalid/expired") | ||||
| @ -58,6 +57,7 @@ def token_secret_key(value: str) -> Optional[User]: | ||||
|     outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) | ||||
|     if not outposts: | ||||
|         return None | ||||
|     LOGGER.info("Authenticating via secret_key") | ||||
|     outpost = outposts.first() | ||||
|     return outpost.user | ||||
|  | ||||
|  | ||||
| @ -33,3 +33,12 @@ class OwnerPermissions(BasePermission): | ||||
|         if owner != request.user: | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|  | ||||
| class OwnerSuperuserPermissions(OwnerPermissions): | ||||
|     """Similar to OwnerPermissions, except always allow access for superusers""" | ||||
|  | ||||
|     def has_object_permission(self, request: Request, view, obj: Model) -> bool: | ||||
|         if request.user.is_superuser: | ||||
|             return True | ||||
|         return super().has_object_permission(request, view, obj) | ||||
|  | ||||
| @ -5,6 +5,9 @@ from typing import Callable, Optional | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def permission_required(perm: Optional[str] = None, other_perms: Optional[list[str]] = None): | ||||
| @ -18,10 +21,12 @@ def permission_required(perm: Optional[str] = None, other_perms: Optional[list[s | ||||
|             if perm: | ||||
|                 obj = self.get_object() | ||||
|                 if not request.user.has_perm(perm, obj): | ||||
|                     LOGGER.debug("denying access for object", user=request.user, perm=perm, obj=obj) | ||||
|                     return self.permission_denied(request) | ||||
|             if other_perms: | ||||
|                 for other_perm in other_perms: | ||||
|                     if not request.user.has_perm(other_perm): | ||||
|                         LOGGER.debug("denying access for other", user=request.user, perm=perm) | ||||
|                         return self.permission_denied(request) | ||||
|             return func(self, request, *args, **kwargs) | ||||
|  | ||||
|  | ||||
| @ -11,7 +11,7 @@ from drf_spectacular.types import OpenApiTypes | ||||
|  | ||||
|  | ||||
| def build_standard_type(obj, **kwargs): | ||||
|     """Build a basic type with optional add ons.""" | ||||
|     """Build a basic type with optional add owns.""" | ||||
|     schema = build_basic_type(obj) | ||||
|     schema.update(kwargs) | ||||
|     return schema | ||||
| @ -31,7 +31,7 @@ VALIDATION_ERROR = build_object_type( | ||||
|         "non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)), | ||||
|         "code": build_standard_type(OpenApiTypes.STR), | ||||
|     }, | ||||
|     required=["detail"], | ||||
|     required=[], | ||||
|     additionalProperties={}, | ||||
| ) | ||||
|  | ||||
|  | ||||
							
								
								
									
										19
									
								
								authentik/api/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								authentik/api/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| """API tasks""" | ||||
|  | ||||
| from authentik.lib.utils.http import get_http_session | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| SENTRY_SESSION = get_http_session() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def sentry_proxy(payload: str): | ||||
|     """Relay data to sentry""" | ||||
|     SENTRY_SESSION.post( | ||||
|         "https://sentry.beryju.org/api/8/envelope/", | ||||
|         data=payload, | ||||
|         headers={ | ||||
|             "Content-Type": "application/octet-stream", | ||||
|         }, | ||||
|         timeout=10, | ||||
|     ) | ||||
| @ -63,7 +63,7 @@ class ConfigView(APIView): | ||||
|  | ||||
|     @extend_schema(responses={200: ConfigSerializer(many=False)}) | ||||
|     def get(self, request: Request) -> Response: | ||||
|         """Retrive public configuration options""" | ||||
|         """Retrieve public configuration options""" | ||||
|         config = ConfigSerializer( | ||||
|             { | ||||
|                 "error_reporting_enabled": CONFIG.y("error_reporting.enabled"), | ||||
|  | ||||
| @ -4,17 +4,19 @@ from json import loads | ||||
| from django.conf import settings | ||||
| from django.http.request import HttpRequest | ||||
| from django.http.response import HttpResponse | ||||
| from requests import post | ||||
| from requests.exceptions import RequestException | ||||
| from rest_framework.authentication import SessionAuthentication | ||||
| from rest_framework.parsers import BaseParser | ||||
| from rest_framework.permissions import AllowAny | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.throttling import AnonRateThrottle | ||||
| from rest_framework.views import APIView | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.tasks import sentry_proxy | ||||
| from authentik.lib.config import CONFIG | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class PlainTextParser(BaseParser): | ||||
|     """Plain text parser.""" | ||||
| @ -46,21 +48,18 @@ class SentryTunnelView(APIView): | ||||
|         """Sentry tunnel, to prevent ad blockers from blocking sentry""" | ||||
|         # Only allow usage of this endpoint when error reporting is enabled | ||||
|         if not CONFIG.y_bool("error_reporting.enabled", False): | ||||
|             LOGGER.debug("error reporting disabled") | ||||
|             return HttpResponse(status=400) | ||||
|         # Body is 2 json objects separated by \n | ||||
|         full_body = request.body | ||||
|         header = loads(full_body.splitlines()[0]) | ||||
|         lines = full_body.splitlines() | ||||
|         if len(lines) < 1: | ||||
|             return HttpResponse(status=400) | ||||
|         header = loads(lines[0]) | ||||
|         # Check that the DSN is what we expect | ||||
|         dsn = header.get("dsn", "") | ||||
|         if dsn != settings.SENTRY_DSN: | ||||
|             LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN) | ||||
|             return HttpResponse(status=400) | ||||
|         response = post( | ||||
|             "https://sentry.beryju.org/api/8/envelope/", | ||||
|             data=full_body, | ||||
|             headers={"Content-Type": "application/octet-stream"}, | ||||
|         ) | ||||
|         try: | ||||
|             response.raise_for_status() | ||||
|         except RequestException: | ||||
|             return HttpResponse(status=500) | ||||
|         return HttpResponse(status=response.status_code) | ||||
|         sentry_proxy.delay(full_body.decode()) | ||||
|         return HttpResponse(status=204) | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| """api v3 urls""" | ||||
| from django.urls import path | ||||
| from django.views.decorators.csrf import csrf_exempt | ||||
| from django.views.decorators.cache import cache_page | ||||
| from drf_spectacular.views import SpectacularAPIView | ||||
| from rest_framework import routers | ||||
|  | ||||
| @ -24,6 +24,7 @@ from authentik.core.api.users import UserViewSet | ||||
| from authentik.crypto.api import CertificateKeyPairViewSet | ||||
| from authentik.events.api.event import EventViewSet | ||||
| from authentik.events.api.notification import NotificationViewSet | ||||
| from authentik.events.api.notification_mapping import NotificationWebhookMappingViewSet | ||||
| from authentik.events.api.notification_rule import NotificationRuleViewSet | ||||
| from authentik.events.api.notification_transport import NotificationTransportViewSet | ||||
| from authentik.flows.api.bindings import FlowStageBindingViewSet | ||||
| @ -98,6 +99,7 @@ from authentik.stages.user_write.api import UserWriteStageViewSet | ||||
| from authentik.tenants.api import TenantViewSet | ||||
|  | ||||
| router = routers.DefaultRouter() | ||||
| router.include_format_suffixes = False | ||||
|  | ||||
| router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks") | ||||
| router.register("admin/apps", AppsViewSet, basename="apps") | ||||
| @ -159,6 +161,7 @@ router.register("propertymappings/all", PropertyMappingViewSet) | ||||
| router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) | ||||
| router.register("propertymappings/saml", SAMLPropertyMappingViewSet) | ||||
| router.register("propertymappings/scope", ScopeMappingViewSet) | ||||
| router.register("propertymappings/notification", NotificationWebhookMappingViewSet) | ||||
|  | ||||
| router.register("authenticators/duo", DuoDeviceViewSet) | ||||
| router.register("authenticators/static", StaticDeviceViewSet) | ||||
| @ -225,7 +228,7 @@ urlpatterns = ( | ||||
|             FlowExecutorView.as_view(), | ||||
|             name="flow-executor", | ||||
|         ), | ||||
|         path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"), | ||||
|         path("schema/", SpectacularAPIView.as_view(), name="schema"), | ||||
|         path("sentry/", SentryTunnelView.as_view(), name="sentry"), | ||||
|         path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"), | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| @ -67,7 +67,7 @@ class ApplicationSerializer(ModelSerializer): | ||||
| class ApplicationViewSet(UsedByMixin, ModelViewSet): | ||||
|     """Application Viewset""" | ||||
|  | ||||
|     queryset = Application.objects.all() | ||||
|     queryset = Application.objects.all().prefetch_related("provider") | ||||
|     serializer_class = ApplicationSerializer | ||||
|     search_fields = [ | ||||
|         "name", | ||||
|  | ||||
| @ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
| from ua_parser import user_agent_parser | ||||
|  | ||||
| from authentik.api.authorization import OwnerSuperuserPermissions | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.models import AuthenticatedSession | ||||
| from authentik.events.geo import GEOIP_READER, GeoIPDict | ||||
| @ -102,11 +103,8 @@ class AuthenticatedSessionViewSet( | ||||
|     search_fields = ["user__username", "last_ip", "last_user_agent"] | ||||
|     filterset_fields = ["user__username", "last_ip", "last_user_agent"] | ||||
|     ordering = ["user__username"] | ||||
|     filter_backends = [ | ||||
|         DjangoFilterBackend, | ||||
|         OrderingFilter, | ||||
|         SearchFilter, | ||||
|     ] | ||||
|     permission_classes = [OwnerSuperuserPermissions] | ||||
|     filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         user = self.request.user if self.request else get_anonymous_user() | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| from django.db.models.query import QuerySet | ||||
| from django_filters.filters import ModelMultipleChoiceFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| from rest_framework.fields import BooleanField, CharField, JSONField | ||||
| from rest_framework.fields import CharField, JSONField | ||||
| from rest_framework.serializers import ListSerializer, ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from rest_framework_guardian.filters import ObjectPermissionsFilter | ||||
| @ -15,7 +15,6 @@ from authentik.core.models import Group, User | ||||
| class GroupMemberSerializer(ModelSerializer): | ||||
|     """Stripped down user serializer to show relevant users for groups""" | ||||
|  | ||||
|     is_superuser = BooleanField(read_only=True) | ||||
|     avatar = CharField(read_only=True) | ||||
|     attributes = JSONField(validators=[is_dict], required=False) | ||||
|     uid = CharField(read_only=True) | ||||
| @ -29,7 +28,6 @@ class GroupMemberSerializer(ModelSerializer): | ||||
|             "name", | ||||
|             "is_active", | ||||
|             "last_login", | ||||
|             "is_superuser", | ||||
|             "email", | ||||
|             "avatar", | ||||
|             "attributes", | ||||
| @ -81,7 +79,7 @@ class GroupFilter(FilterSet): | ||||
| class GroupViewSet(UsedByMixin, ModelViewSet): | ||||
|     """Group Viewset""" | ||||
|  | ||||
|     queryset = Group.objects.all() | ||||
|     queryset = Group.objects.all().select_related("parent").prefetch_related("users") | ||||
|     serializer_class = GroupSerializer | ||||
|     search_fields = ["name", "is_superuser"] | ||||
|     filterset_class = GroupFilter | ||||
|  | ||||
| @ -95,7 +95,9 @@ class SourceViewSet( | ||||
|     @action(detail=False, pagination_class=None, filter_backends=[]) | ||||
|     def user_settings(self, request: Request) -> Response: | ||||
|         """Get all sources the user can configure""" | ||||
|         _all_sources: Iterable[Source] = Source.objects.filter(enabled=True).select_subclasses() | ||||
|         _all_sources: Iterable[Source] = ( | ||||
|             Source.objects.filter(enabled=True).select_subclasses().order_by("name") | ||||
|         ) | ||||
|         matching_sources: list[UserSettingSerializer] = [] | ||||
|         for source in _all_sources: | ||||
|             user_settings = source.ui_user_settings | ||||
|  | ||||
| @ -2,15 +2,19 @@ | ||||
| from typing import Any | ||||
|  | ||||
| from django.http.response import Http404 | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.fields import CharField | ||||
| from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.api.authorization import OwnerSuperuserPermissions | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.users import UserSerializer | ||||
| @ -78,14 +82,25 @@ class TokenViewSet(UsedByMixin, ModelViewSet): | ||||
|         "description", | ||||
|         "expires", | ||||
|         "expiring", | ||||
|         "managed", | ||||
|     ] | ||||
|     ordering = ["expires"] | ||||
|     ordering = ["identifier", "expires"] | ||||
|     permission_classes = [OwnerSuperuserPermissions] | ||||
|     filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         user = self.request.user if self.request else get_anonymous_user() | ||||
|         if user.is_superuser: | ||||
|             return super().get_queryset() | ||||
|         return super().get_queryset().filter(user=user.pk) | ||||
|  | ||||
|     def perform_create(self, serializer: TokenSerializer): | ||||
|         serializer.save( | ||||
|             user=self.request.user, | ||||
|             expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True), | ||||
|         ) | ||||
|         if not self.request.user.is_superuser: | ||||
|             return serializer.save( | ||||
|                 user=self.request.user, | ||||
|                 expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True), | ||||
|             ) | ||||
|         return super().perform_create(serializer) | ||||
|  | ||||
|     @permission_required("authentik_core.view_token_key") | ||||
|     @extend_schema( | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """User API Views""" | ||||
| from datetime import timedelta | ||||
| from json import loads | ||||
| from typing import Optional | ||||
|  | ||||
| @ -7,6 +8,8 @@ from django.db.transaction import atomic | ||||
| from django.db.utils import IntegrityError | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.http import urlencode | ||||
| from django.utils.text import slugify | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext as _ | ||||
| from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| @ -87,6 +90,9 @@ class UserSerializer(ModelSerializer): | ||||
|             "attributes", | ||||
|             "uid", | ||||
|         ] | ||||
|         extra_kwargs = { | ||||
|             "name": {"allow_blank": True}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class UserSelfSerializer(ModelSerializer): | ||||
| @ -95,9 +101,25 @@ class UserSelfSerializer(ModelSerializer): | ||||
|  | ||||
|     is_superuser = BooleanField(read_only=True) | ||||
|     avatar = CharField(read_only=True) | ||||
|     groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups") | ||||
|     groups = SerializerMethodField() | ||||
|     uid = CharField(read_only=True) | ||||
|  | ||||
|     @extend_schema_field( | ||||
|         ListSerializer( | ||||
|             child=inline_serializer( | ||||
|                 "UserSelfGroups", | ||||
|                 {"name": CharField(read_only=True), "pk": CharField(read_only=True)}, | ||||
|             ) | ||||
|         ) | ||||
|     ) | ||||
|     def get_groups(self, user: User): | ||||
|         """Return only the group names a user is member of""" | ||||
|         for group in user.ak_groups.all(): | ||||
|             yield { | ||||
|                 "name": group.name, | ||||
|                 "pk": group.pk, | ||||
|             } | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = User | ||||
| @ -114,6 +136,7 @@ class UserSelfSerializer(ModelSerializer): | ||||
|         ] | ||||
|         extra_kwargs = { | ||||
|             "is_active": {"read_only": True}, | ||||
|             "name": {"allow_blank": True}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| @ -205,6 +228,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|     """User Viewset""" | ||||
|  | ||||
|     queryset = User.objects.none() | ||||
|     ordering = ["username"] | ||||
|     serializer_class = UserSerializer | ||||
|     search_fields = ["username", "name", "is_active", "email"] | ||||
|     filterset_class = UsersFilter | ||||
| @ -271,9 +295,10 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|                     ) | ||||
|                     group.users.add(user) | ||||
|                 token = Token.objects.create( | ||||
|                     identifier=f"service-account-{username}-password", | ||||
|                     identifier=slugify(f"service-account-{username}-password"), | ||||
|                     intent=TokenIntents.INTENT_APP_PASSWORD, | ||||
|                     user=user, | ||||
|                     expires=now() + timedelta(days=360), | ||||
|                 ) | ||||
|                 return Response({"username": user.username, "token": token.key}) | ||||
|             except (IntegrityError) as exc: | ||||
| @ -304,7 +329,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         """Allow users to change information on their own profile""" | ||||
|         data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data) | ||||
|         if not data.is_valid(): | ||||
|             return Response(data.errors) | ||||
|             return Response(data.errors, status=400) | ||||
|         new_user = data.save() | ||||
|         # If we're impersonating, we need to update that user object | ||||
|         # since it caches the full object | ||||
|  | ||||
| @ -26,7 +26,7 @@ class Migration(migrations.Migration): | ||||
|                     ), | ||||
|                     ( | ||||
|                         "username_link", | ||||
|                         "Link to a user with identical username address. Can have security implications when a username is used with another source.", | ||||
|                         "Link to a user with identical username. Can have security implications when a username is used with another source.", | ||||
|                     ), | ||||
|                     ( | ||||
|                         "username_deny", | ||||
|  | ||||
| @ -283,7 +283,7 @@ class SourceUserMatchingModes(models.TextChoices): | ||||
|     ) | ||||
|     USERNAME_LINK = "username_link", _( | ||||
|         ( | ||||
|             "Link to a user with identical username address. Can have security implications " | ||||
|             "Link to a user with identical username. Can have security implications " | ||||
|             "when a username is used with another source." | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
| @ -184,7 +184,7 @@ class SourceFlowManager: | ||||
|         # Ensure redirect is carried through when user was trying to | ||||
|         # authorize application | ||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||
|             NEXT_ARG_NAME, "authentik_core:if-admin" | ||||
|             NEXT_ARG_NAME, "authentik_core:if-user" | ||||
|         ) | ||||
|         kwargs.update( | ||||
|             { | ||||
| @ -243,9 +243,9 @@ class SourceFlowManager: | ||||
|             return self.handle_auth_user(connection) | ||||
|         return redirect( | ||||
|             reverse( | ||||
|                 "authentik_core:if-admin", | ||||
|                 "authentik_core:if-user", | ||||
|             ) | ||||
|             + f"#/user;page-{self.source.slug}" | ||||
|             + f"#/settings;page-{self.source.slug}" | ||||
|         ) | ||||
|  | ||||
|     def handle_enroll( | ||||
|  | ||||
| @ -28,3 +28,7 @@ class PostUserEnrollmentStage(StageView): | ||||
|             source=connection.source, | ||||
|         ).from_http(self.request) | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
|     def post(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Wrapper for post requests""" | ||||
|         return self.get(request) | ||||
|  | ||||
| @ -8,16 +8,15 @@ | ||||
|         <meta charset="UTF-8"> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||
|         <title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title> | ||||
|         <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}"> | ||||
|         {% block head_before %} | ||||
|         {% endblock %} | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}"> | ||||
|         <script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script> | ||||
|         <script>window["polymerSkipLoadingFontRoboto"] = true;</script> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> | ||||
|         <script src="{% static 'dist/poly.js' %}" type="module"></script> | ||||
|         {% block head %} | ||||
|         {% endblock %} | ||||
|     </head> | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block head %} | ||||
| <script src="{% static 'dist/AdminInterface.js' %}?v={{ ak_version }}" type="module"></script> | ||||
| <script src="{% static 'dist/AdminInterface.js' %}" type="module"></script> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
|  | ||||
| @ -21,7 +21,7 @@ You've logged out of {{ application }}. | ||||
|         {% endblocktrans %} | ||||
|     </p> | ||||
|  | ||||
|     <a id="ak-back-home" href="{% url 'authentik_core:if-admin' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a> | ||||
|     <a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a> | ||||
|  | ||||
|     <a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a> | ||||
|  | ||||
|  | ||||
| @ -4,13 +4,14 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block head_before %} | ||||
| {{ block.super }} | ||||
| {% if flow.compatibility_mode %} | ||||
| <script>ShadyDOM = { force: !navigator.webdriver };</script> | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block head %} | ||||
| <script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script> | ||||
| <script src="{% static 'dist/FlowInterface.js' %}" type="module"></script> | ||||
| <style> | ||||
| .pf-c-background-image::before { | ||||
|     --ak-flow-background: url("{{ flow.background_url }}"); | ||||
|  | ||||
							
								
								
									
										28
									
								
								authentik/core/templates/if/user.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								authentik/core/templates/if/user.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| {% extends "base/skeleton.html" %} | ||||
|  | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block head %} | ||||
| <script src="{% static 'dist/UserInterface.js' %}" type="module"></script> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <ak-message-container></ak-message-container> | ||||
| <ak-interface-user> | ||||
|     <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||
|         <div class="pf-c-empty-state" style="height: 100vh;"> | ||||
|             <div class="pf-c-empty-state__content"> | ||||
|                 <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> | ||||
|                     <span class="pf-c-spinner__clipper"></span> | ||||
|                     <span class="pf-c-spinner__lead-ball"></span> | ||||
|                     <span class="pf-c-spinner__tail-ball"></span> | ||||
|                 </span> | ||||
|                 <h1 class="pf-c-title pf-m-lg"> | ||||
|                     {% trans "Loading..." %} | ||||
|                 </h1> | ||||
|             </div> | ||||
|         </div> | ||||
|     </section> | ||||
| </ak-interface-user> | ||||
| {% endblock %} | ||||
| @ -4,7 +4,7 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block head_before %} | ||||
| <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}"> | ||||
| <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block head %} | ||||
|  | ||||
| @ -58,4 +58,4 @@ class TestImpersonation(TestCase): | ||||
|         self.client.force_login(self.other_user) | ||||
|  | ||||
|         response = self.client.get(reverse("authentik_core:impersonate-end")) | ||||
|         self.assertRedirects(response, reverse("authentik_core:if-admin")) | ||||
|         self.assertRedirects(response, reverse("authentik_core:if-user")) | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| """Test token API""" | ||||
| from json import loads | ||||
|  | ||||
| from django.urls.base import reverse | ||||
| from django.utils.timezone import now | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| @ -13,7 +15,8 @@ class TestTokenAPI(APITestCase): | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.user = User.objects.get(username="akadmin") | ||||
|         self.user = User.objects.create(username="testuser") | ||||
|         self.admin = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_token_create(self): | ||||
| @ -55,3 +58,29 @@ class TestTokenAPI(APITestCase): | ||||
|         clean_expired_models.delay().get() | ||||
|         token.refresh_from_db() | ||||
|         self.assertNotEqual(key, token.key) | ||||
|  | ||||
|     def test_list(self): | ||||
|         """Test Token List (Test normal authentication)""" | ||||
|         token_should: Token = Token.objects.create( | ||||
|             identifier="test", expiring=False, user=self.user | ||||
|         ) | ||||
|         Token.objects.create(identifier="test-2", expiring=False, user=get_anonymous_user()) | ||||
|         response = self.client.get(reverse(("authentik_api:token-list"))) | ||||
|         body = loads(response.content) | ||||
|         self.assertEqual(len(body["results"]), 1) | ||||
|         self.assertEqual(body["results"][0]["identifier"], token_should.identifier) | ||||
|  | ||||
|     def test_list_admin(self): | ||||
|         """Test Token List (Test with admin auth)""" | ||||
|         self.client.force_login(self.admin) | ||||
|         token_should: Token = Token.objects.create( | ||||
|             identifier="test", expiring=False, user=self.user | ||||
|         ) | ||||
|         token_should_not: Token = Token.objects.create( | ||||
|             identifier="test-2", expiring=False, user=get_anonymous_user() | ||||
|         ) | ||||
|         response = self.client.get(reverse(("authentik_api:token-list"))) | ||||
|         body = loads(response.content) | ||||
|         self.assertEqual(len(body["results"]), 2) | ||||
|         self.assertEqual(body["results"][0]["identifier"], token_should.identifier) | ||||
|         self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier) | ||||
|  | ||||
| @ -12,7 +12,7 @@ from authentik.core.views.session import EndSessionView | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "", | ||||
|         login_required(RedirectView.as_view(pattern_name="authentik_core:if-admin")), | ||||
|         login_required(RedirectView.as_view(pattern_name="authentik_core:if-user")), | ||||
|         name="root-redirect", | ||||
|     ), | ||||
|     # Impersonation | ||||
| @ -32,6 +32,11 @@ urlpatterns = [ | ||||
|         ensure_csrf_cookie(TemplateView.as_view(template_name="if/admin.html")), | ||||
|         name="if-admin", | ||||
|     ), | ||||
|     path( | ||||
|         "if/user/", | ||||
|         ensure_csrf_cookie(TemplateView.as_view(template_name="if/user.html")), | ||||
|         name="if-user", | ||||
|     ), | ||||
|     path( | ||||
|         "if/flow/<slug:flow_slug>/", | ||||
|         ensure_csrf_cookie(FlowInterfaceView.as_view()), | ||||
|  | ||||
| @ -28,7 +28,7 @@ class ImpersonateInitView(View): | ||||
|  | ||||
|         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) | ||||
|  | ||||
|         return redirect("authentik_core:if-admin") | ||||
|         return redirect("authentik_core:if-user") | ||||
|  | ||||
|  | ||||
| class ImpersonateEndView(View): | ||||
| @ -41,7 +41,7 @@ class ImpersonateEndView(View): | ||||
|             or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session | ||||
|         ): | ||||
|             LOGGER.debug("Can't end impersonation", user=request.user) | ||||
|             return redirect("authentik_core:if-admin") | ||||
|             return redirect("authentik_core:if-user") | ||||
|  | ||||
|         original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER] | ||||
|  | ||||
|  | ||||
| @ -78,9 +78,7 @@ class CertificateKeyPair(CreatedUpdatedModel): | ||||
|     @property | ||||
|     def kid(self): | ||||
|         """Get Key ID used for JWKS""" | ||||
|         return "{0}".format( | ||||
|             md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else ""  # nosec | ||||
|         ) | ||||
|         return md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else ""  # nosec | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"Certificate-Key Pair {self.name}" | ||||
|  | ||||
| @ -1,8 +1,13 @@ | ||||
| """Notification API Views""" | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from rest_framework import mixins | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import ReadOnlyField | ||||
| from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| @ -53,3 +58,18 @@ class NotificationViewSet( | ||||
|     ] | ||||
|     permission_classes = [OwnerPermissions] | ||||
|     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|  | ||||
|     @extend_schema( | ||||
|         request=OpenApiTypes.NONE, | ||||
|         responses={ | ||||
|             204: OpenApiResponse(description="Marked tasks as read successfully."), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=False, methods=["post"]) | ||||
|     def mark_all_seen(self, request: Request) -> Response: | ||||
|         """Mark all the user's notifications as seen""" | ||||
|         notifications = Notification.objects.filter(user=request.user) | ||||
|         for notification in notifications: | ||||
|             notification.seen = True | ||||
|         Notification.objects.bulk_update(notifications, ["seen"]) | ||||
|         return Response({}, status=204) | ||||
|  | ||||
							
								
								
									
										28
									
								
								authentik/events/api/notification_mapping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								authentik/events/api/notification_mapping.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| """NotificationWebhookMapping API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.events.models import NotificationWebhookMapping | ||||
|  | ||||
|  | ||||
| class NotificationWebhookMappingSerializer(ModelSerializer): | ||||
|     """NotificationWebhookMapping Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = NotificationWebhookMapping | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "expression", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class NotificationWebhookMappingViewSet(UsedByMixin, ModelViewSet): | ||||
|     """NotificationWebhookMapping Viewset""" | ||||
|  | ||||
|     queryset = NotificationWebhookMapping.objects.all() | ||||
|     serializer_class = NotificationWebhookMappingSerializer | ||||
|     filterset_fields = ["name"] | ||||
|     ordering = ["name"] | ||||
| @ -1,7 +1,10 @@ | ||||
| """NotificationTransport API Views""" | ||||
| from typing import Any | ||||
|  | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.fields import CharField, ListField, SerializerMethodField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| @ -29,6 +32,14 @@ class NotificationTransportSerializer(ModelSerializer): | ||||
|         """Return selected mode with a UI Label""" | ||||
|         return TransportMode(instance.mode).label | ||||
|  | ||||
|     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: | ||||
|         """Ensure the required fields are set.""" | ||||
|         mode = attrs.get("mode") | ||||
|         if mode in [TransportMode.WEBHOOK, TransportMode.WEBHOOK_SLACK]: | ||||
|             if "webhook_url" not in attrs or attrs.get("webhook_url", "") == "": | ||||
|                 raise ValidationError("Webhook URL may not be empty.") | ||||
|         return attrs | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = NotificationTransport | ||||
| @ -38,6 +49,7 @@ class NotificationTransportSerializer(ModelSerializer): | ||||
|             "mode", | ||||
|             "mode_verbose", | ||||
|             "webhook_url", | ||||
|             "webhook_mapping", | ||||
|             "send_once", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| @ -1,10 +1,7 @@ | ||||
| """authentik events app""" | ||||
| from datetime import timedelta | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from django.db import ProgrammingError | ||||
| from django.utils.timezone import now | ||||
|  | ||||
|  | ||||
| class AuthentikEventsConfig(AppConfig): | ||||
| @ -16,12 +13,3 @@ class AuthentikEventsConfig(AppConfig): | ||||
|  | ||||
|     def ready(self): | ||||
|         import_module("authentik.events.signals") | ||||
|         try: | ||||
|             from authentik.events.models import Event | ||||
|  | ||||
|             date_from = now() - timedelta(days=1) | ||||
|  | ||||
|             for event in Event.objects.filter(created__gte=date_from): | ||||
|                 event._set_prom_metrics() | ||||
|         except ProgrammingError: | ||||
|             pass | ||||
|  | ||||
							
								
								
									
										46
									
								
								authentik/events/migrations/0018_auto_20210911_2217.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								authentik/events/migrations/0018_auto_20210911_2217.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-11 22:17 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_core", "0028_alter_token_intent"), | ||||
|         ("authentik_events", "0017_alter_event_action"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="NotificationWebhookMapping", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "propertymapping_ptr", | ||||
|                     models.OneToOneField( | ||||
|                         auto_created=True, | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         parent_link=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         to="authentik_core.propertymapping", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Notification Webhook Mapping", | ||||
|                 "verbose_name_plural": "Notification Webhook Mappings", | ||||
|             }, | ||||
|             bases=("authentik_core.propertymapping",), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="notificationtransport", | ||||
|             name="webhook_mapping", | ||||
|             field=models.ForeignKey( | ||||
|                 default=None, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||
|                 to="authentik_events.notificationwebhookmapping", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,19 @@ | ||||
| # Generated by Django 3.2.7 on 2021-10-04 15:31 | ||||
|  | ||||
| import django.core.validators | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0018_auto_20210911_2217"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="notificationtransport", | ||||
|             name="webhook_url", | ||||
|             field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]), | ||||
|         ), | ||||
|     ] | ||||
| @ -2,25 +2,26 @@ | ||||
| from datetime import timedelta | ||||
| from inspect import getmodule, stack | ||||
| from smtplib import SMTPException | ||||
| from typing import Optional, Union | ||||
| from typing import TYPE_CHECKING, Optional, Type, Union | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.validators import URLValidator | ||||
| from django.db import models | ||||
| from django.http import HttpRequest | ||||
| from django.http.request import QueryDict | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext as _ | ||||
| from prometheus_client import Gauge | ||||
| from requests import RequestException, post | ||||
| from requests import RequestException | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import __version__ | ||||
| from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER | ||||
| from authentik.core.models import ExpiringModel, Group, User | ||||
| from authentik.core.models import ExpiringModel, Group, PropertyMapping, User | ||||
| from authentik.events.geo import GEOIP_READER | ||||
| from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
| from authentik.lib.utils.http import get_client_ip, get_http_session | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.policies.models import PolicyBindingModel | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
| @ -28,11 +29,8 @@ from authentik.tenants.models import Tenant | ||||
| from authentik.tenants.utils import DEFAULT_TENANT | ||||
|  | ||||
| LOGGER = get_logger("authentik.events") | ||||
| GAUGE_EVENTS = Gauge( | ||||
|     "authentik_events", | ||||
|     "Events in authentik", | ||||
|     ["action", "user_username", "app", "client_ip"], | ||||
| ) | ||||
| if TYPE_CHECKING: | ||||
|     from rest_framework.serializers import Serializer | ||||
|  | ||||
|  | ||||
| def default_event_duration(): | ||||
| @ -143,8 +141,9 @@ class Event(ExpiringModel): | ||||
|         `user` arguments optionally overrides user from requests.""" | ||||
|         if request: | ||||
|             self.context["http_request"] = { | ||||
|                 "path": request.get_full_path(), | ||||
|                 "path": request.path, | ||||
|                 "method": request.method, | ||||
|                 "args": QueryDict(request.META.get("QUERY_STRING", "")), | ||||
|             } | ||||
|         if hasattr(request, "tenant"): | ||||
|             tenant: Tenant = request.tenant | ||||
| @ -182,14 +181,6 @@ class Event(ExpiringModel): | ||||
|             return | ||||
|         self.context["geo"] = city | ||||
|  | ||||
|     def _set_prom_metrics(self): | ||||
|         GAUGE_EVENTS.labels( | ||||
|             action=self.action, | ||||
|             user_username=self.user.get("username"), | ||||
|             app=self.app, | ||||
|             client_ip=self.client_ip, | ||||
|         ).set(self.created.timestamp()) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self._state.adding: | ||||
|             LOGGER.debug( | ||||
| @ -200,7 +191,6 @@ class Event(ExpiringModel): | ||||
|                 user=self.user, | ||||
|             ) | ||||
|         super().save(*args, **kwargs) | ||||
|         self._set_prom_metrics() | ||||
|  | ||||
|     @property | ||||
|     def summary(self) -> str: | ||||
| @ -234,7 +224,10 @@ class NotificationTransport(models.Model): | ||||
|     name = models.TextField(unique=True) | ||||
|     mode = models.TextField(choices=TransportMode.choices) | ||||
|  | ||||
|     webhook_url = models.TextField(blank=True) | ||||
|     webhook_url = models.TextField(blank=True, validators=[URLValidator()]) | ||||
|     webhook_mapping = models.ForeignKey( | ||||
|         "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None | ||||
|     ) | ||||
|     send_once = models.BooleanField( | ||||
|         default=False, | ||||
|         help_text=_( | ||||
| @ -254,15 +247,22 @@ class NotificationTransport(models.Model): | ||||
|  | ||||
|     def send_webhook(self, notification: "Notification") -> list[str]: | ||||
|         """Send notification to generic webhook""" | ||||
|         default_body = { | ||||
|             "body": notification.body, | ||||
|             "severity": notification.severity, | ||||
|             "user_email": notification.user.email, | ||||
|             "user_username": notification.user.username, | ||||
|         } | ||||
|         if self.webhook_mapping: | ||||
|             default_body = self.webhook_mapping.evaluate( | ||||
|                 user=notification.user, | ||||
|                 request=None, | ||||
|                 notification=notification, | ||||
|             ) | ||||
|         try: | ||||
|             response = post( | ||||
|             response = get_http_session().post( | ||||
|                 self.webhook_url, | ||||
|                 json={ | ||||
|                     "body": notification.body, | ||||
|                     "severity": notification.severity, | ||||
|                     "user_email": notification.user.email, | ||||
|                     "user_username": notification.user.username, | ||||
|                 }, | ||||
|                 json=default_body, | ||||
|             ) | ||||
|             response.raise_for_status() | ||||
|         except RequestException as exc: | ||||
| @ -312,7 +312,7 @@ class NotificationTransport(models.Model): | ||||
|         if notification.event: | ||||
|             body["attachments"][0]["title"] = notification.event.action | ||||
|         try: | ||||
|             response = post(self.webhook_url, json=body) | ||||
|             response = get_http_session().post(self.webhook_url, json=body) | ||||
|             response.raise_for_status() | ||||
|         except RequestException as exc: | ||||
|             text = exc.response.text if exc.response else str(exc) | ||||
| @ -429,3 +429,25 @@ class NotificationRule(PolicyBindingModel): | ||||
|  | ||||
|         verbose_name = _("Notification Rule") | ||||
|         verbose_name_plural = _("Notification Rules") | ||||
|  | ||||
|  | ||||
| class NotificationWebhookMapping(PropertyMapping): | ||||
|     """Modify the schema and layout of the webhook being sent""" | ||||
|  | ||||
|     @property | ||||
|     def component(self) -> str: | ||||
|         return "ak-property-mapping-notification-form" | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> Type["Serializer"]: | ||||
|         from authentik.events.api.notification_mapping import NotificationWebhookMappingSerializer | ||||
|  | ||||
|         return NotificationWebhookMappingSerializer | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"Notification Webhook Mapping {self.name}" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Notification Webhook Mapping") | ||||
|         verbose_name_plural = _("Notification Webhook Mappings") | ||||
|  | ||||
| @ -3,7 +3,6 @@ from dataclasses import dataclass, field | ||||
| from datetime import datetime | ||||
| from enum import Enum | ||||
| from timeit import default_timer | ||||
| from traceback import format_tb | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from celery import Task | ||||
| @ -42,8 +41,7 @@ class TaskResult: | ||||
|  | ||||
|     def with_error(self, exc: Exception) -> "TaskResult": | ||||
|         """Since errors might not always be pickle-able, set the traceback""" | ||||
|         self.messages.extend(format_tb(exc.__traceback__)) | ||||
|         self.messages.append(str(exc)) | ||||
|         self.messages.extend(exception_to_string(exc).splitlines()) | ||||
|         return self | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -4,15 +4,21 @@ from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.models import ( | ||||
|     Event, | ||||
|     EventAction, | ||||
|     Notification, | ||||
|     NotificationSeverity, | ||||
|     TransportMode, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TestEventsAPI(APITestCase): | ||||
|     """Test Event API""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         user = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(user) | ||||
|         self.user = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_top_n(self): | ||||
|         """Test top_per_user""" | ||||
| @ -30,3 +36,34 @@ class TestEventsAPI(APITestCase): | ||||
|             reverse("authentik_api:event-actions"), | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_notifications(self): | ||||
|         """Test notifications""" | ||||
|         notification = Notification.objects.create( | ||||
|             user=self.user, severity=NotificationSeverity.ALERT, body="", seen=False | ||||
|         ) | ||||
|         self.client.post( | ||||
|             reverse("authentik_api:notification-mark-all-seen"), | ||||
|         ) | ||||
|         notification.refresh_from_db() | ||||
|         self.assertTrue(notification.seen) | ||||
|  | ||||
|     def test_transport(self): | ||||
|         """Test transport API""" | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:notificationtransport-list"), | ||||
|             data={ | ||||
|                 "name": "foo-with", | ||||
|                 "mode": TransportMode.WEBHOOK, | ||||
|                 "webhook_url": "http://foo.com", | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 201) | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:notificationtransport-list"), | ||||
|             data={ | ||||
|                 "name": "foo-without", | ||||
|                 "mode": TransportMode.WEBHOOK, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|  | ||||
| @ -77,7 +77,7 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]: | ||||
|     final_dict = {} | ||||
|     for key, value in source.items(): | ||||
|         if is_dataclass(value): | ||||
|             # Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict, | ||||
|             # Because asdict calls `copy.deepcopy(obj)` on everything that's not tuple/dict, | ||||
|             # and deepcopy doesn't work with HttpRequests (neither django nor rest_framework). | ||||
|             # Currently, the only dataclass that actually holds an http request is a PolicyRequest | ||||
|             if isinstance(value, PolicyRequest): | ||||
|  | ||||
| @ -108,6 +108,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet): | ||||
|     queryset = Flow.objects.all() | ||||
|     serializer_class = FlowSerializer | ||||
|     lookup_field = "slug" | ||||
|     ordering = ["slug", "name"] | ||||
|     search_fields = ["name", "slug", "designation", "title"] | ||||
|     filterset_fields = ["flow_uuid", "name", "slug", "designation"] | ||||
|  | ||||
|  | ||||
| @ -86,7 +86,7 @@ class StageViewSet( | ||||
|     @action(detail=False, pagination_class=None, filter_backends=[]) | ||||
|     def user_settings(self, request: Request) -> Response: | ||||
|         """Get all stages the user can configure""" | ||||
|         _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses() | ||||
|         _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses().order_by("name") | ||||
|         matching_stages: list[dict] = [] | ||||
|         for stage in _all_stages: | ||||
|             user_settings = stage.ui_user_settings | ||||
|  | ||||
| @ -57,11 +57,11 @@ class FlowPlan: | ||||
|     markers: list[StageMarker] = field(default_factory=list) | ||||
|  | ||||
|     def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None): | ||||
|         """Append `stage` to all stages, optionall with stage marker""" | ||||
|         """Append `stage` to all stages, optionally with stage marker""" | ||||
|         return self.append(FlowStageBinding(stage=stage), marker) | ||||
|  | ||||
|     def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None): | ||||
|         """Append `stage` to all stages, optionall with stage marker""" | ||||
|         """Append `stage` to all stages, optionally with stage marker""" | ||||
|         self.bindings.append(binding) | ||||
|         self.markers.append(marker or StageMarker()) | ||||
|  | ||||
|  | ||||
							
								
								
									
										31
									
								
								authentik/flows/tests/test_stage_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								authentik/flows/tests/test_stage_views.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| """stage view tests""" | ||||
| from typing import Callable, Type | ||||
|  | ||||
| from django.test import RequestFactory, TestCase | ||||
|  | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views import FlowExecutorView | ||||
| from authentik.lib.utils.reflection import all_subclasses | ||||
|  | ||||
|  | ||||
| class TestViews(TestCase): | ||||
|     """Generic model properties tests""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.factory = RequestFactory() | ||||
|         self.exec = FlowExecutorView(request=self.factory.get("/")) | ||||
|  | ||||
|  | ||||
| def view_tester_factory(view_class: Type[StageView]) -> Callable: | ||||
|     """Test a form""" | ||||
|  | ||||
|     def tester(self: TestViews): | ||||
|         model_class = view_class(self.exec) | ||||
|         self.assertIsNotNone(model_class.post) | ||||
|         self.assertIsNotNone(model_class.get) | ||||
|  | ||||
|     return tester | ||||
|  | ||||
|  | ||||
| for view in all_subclasses(StageView): | ||||
|     setattr(TestViews, f"test_view_{view.__name__}", view_tester_factory(view)) | ||||
| @ -438,7 +438,7 @@ class TestFlowExecutor(APITestCase): | ||||
|  | ||||
|         # third request, this should trigger the re-evaluate | ||||
|         # A get request will evaluate the policies and this will return stage 4 | ||||
|         # but it won't save it, hence we cant' check the plan | ||||
|         # but it won't save it, hence we can't check the plan | ||||
|         response = self.client.get(exec_url) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual( | ||||
|  | ||||
| @ -11,7 +11,7 @@ from authentik.lib.sentry import SentryIgnoredException | ||||
|  | ||||
|  | ||||
| def get_attrs(obj: SerializerModel) -> dict[str, Any]: | ||||
|     """Get object's attributes via their serializer, and covert it to a normal dict""" | ||||
|     """Get object's attributes via their serializer, and convert it to a normal dict""" | ||||
|     data = dict(obj.serializer(obj).data) | ||||
|     to_remove = ( | ||||
|         "policies", | ||||
|  | ||||
| @ -14,12 +14,7 @@ from django.utils.decorators import method_decorator | ||||
| from django.views.decorators.clickjacking import xframe_options_sameorigin | ||||
| from django.views.generic import View | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import ( | ||||
|     OpenApiParameter, | ||||
|     OpenApiResponse, | ||||
|     PolymorphicProxySerializer, | ||||
|     extend_schema, | ||||
| ) | ||||
| from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema | ||||
| from rest_framework.permissions import AllowAny | ||||
| from rest_framework.views import APIView | ||||
| from sentry_sdk import capture_exception | ||||
| @ -131,12 +126,12 @@ class FlowExecutorView(APIView): | ||||
|  | ||||
|     # pylint: disable=unused-argument, too-many-return-statements | ||||
|     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: | ||||
|         # Early check if theres an active Plan for the current session | ||||
|         # Early check if there's an active Plan for the current session | ||||
|         if SESSION_KEY_PLAN in self.request.session: | ||||
|             self.plan = self.request.session[SESSION_KEY_PLAN] | ||||
|             if self.plan.flow_pk != self.flow.pk.hex: | ||||
|                 self._logger.warning( | ||||
|                     "f(exec): Found existing plan for other flow, deleteing plan", | ||||
|                     "f(exec): Found existing plan for other flow, deleting plan", | ||||
|                 ) | ||||
|                 # Existing plan is deleted from session and instance | ||||
|                 self.plan = None | ||||
| @ -213,9 +208,6 @@ class FlowExecutorView(APIView): | ||||
|                 serializers=challenge_types(), | ||||
|                 resource_type_field_name="component", | ||||
|             ), | ||||
|             404: OpenApiResponse( | ||||
|                 description="No Token found" | ||||
|             ),  # This error can be raised by the email stage | ||||
|         }, | ||||
|         request=OpenApiTypes.NONE, | ||||
|         parameters=[ | ||||
| @ -441,7 +433,7 @@ class ToDefaultFlow(View): | ||||
|             plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] | ||||
|             if plan.flow_pk != flow.pk.hex: | ||||
|                 LOGGER.warning( | ||||
|                     "f(def): Found existing plan for other flow, deleteing plan", | ||||
|                     "f(def): Found existing plan for other flow, deleting plan", | ||||
|                     flow_slug=flow.slug, | ||||
|                 ) | ||||
|                 del self.request.session[SESSION_KEY_PLAN] | ||||
|  | ||||
| @ -9,7 +9,9 @@ postgresql: | ||||
| web: | ||||
|   listen: 0.0.0.0:9000 | ||||
|   listen_tls: 0.0.0.0:9443 | ||||
|   listen_metrics: 0.0.0.0:9300 | ||||
|   load_local_files: false | ||||
|   outpost_port_offset: 0 | ||||
|  | ||||
| redis: | ||||
|   host: localhost | ||||
| @ -54,6 +56,7 @@ outposts: | ||||
|   # %(build_hash)s: Build hash if you're running a beta version | ||||
|   docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s" | ||||
|  | ||||
| disable_update_check: false | ||||
| avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar | ||||
| geoip: "./GeoLite2-City.mmdb" | ||||
|  | ||||
|  | ||||
| @ -4,13 +4,13 @@ from textwrap import indent | ||||
| from typing import Any, Iterable, Optional | ||||
|  | ||||
| from django.core.exceptions import FieldError | ||||
| from requests import Session | ||||
| from rest_framework.serializers import ValidationError | ||||
| from sentry_sdk.hub import Hub | ||||
| from sentry_sdk.tracing import Span | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.lib.utils.http import get_http_session | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -35,7 +35,7 @@ class BaseEvaluator: | ||||
|             "ak_is_group_member": BaseEvaluator.expr_is_group_member, | ||||
|             "ak_user_by": BaseEvaluator.expr_user_by, | ||||
|             "ak_logger": get_logger(), | ||||
|             "requests": Session(), | ||||
|             "requests": get_http_session(), | ||||
|         } | ||||
|         self._context = {} | ||||
|         self._filename = "BaseEvalautor" | ||||
|  | ||||
| @ -93,6 +93,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | ||||
|     if "exc_info" in hint: | ||||
|         _, exc_value, _ = hint["exc_info"] | ||||
|         if isinstance(exc_value, ignored_classes): | ||||
|             LOGGER.debug("dropping exception", exception=exc_value) | ||||
|             return None | ||||
|     if "logger" in event: | ||||
|         if event["logger"] in [ | ||||
|  | ||||
| @ -32,7 +32,7 @@ class TestConfig(TestCase): | ||||
|         config = ConfigLoader() | ||||
|         environ["foo"] = "bar" | ||||
|         self.assertEqual(config.parse_uri("env://foo"), "bar") | ||||
|         self.assertEqual(config.parse_uri("env://fo?bar"), "bar") | ||||
|         self.assertEqual(config.parse_uri("env://foo?bar"), "bar") | ||||
|  | ||||
|     def test_uri_file(self): | ||||
|         """Test URI parsing (file load)""" | ||||
|  | ||||
| @ -27,7 +27,7 @@ class TestHTTP(TestCase): | ||||
|         token = Token.objects.create( | ||||
|             identifier="test", user=self.user, intent=TokenIntents.INTENT_API | ||||
|         ) | ||||
|         # Invalid, non-existant token | ||||
|         # Invalid, non-existent token | ||||
|         request = self.factory.get( | ||||
|             "/", | ||||
|             **{ | ||||
| @ -36,7 +36,7 @@ class TestHTTP(TestCase): | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(get_client_ip(request), "127.0.0.1") | ||||
|         # Invalid, user doesn't have permisions | ||||
|         # Invalid, user doesn't have permissions | ||||
|         request = self.factory.get( | ||||
|             "/", | ||||
|             **{ | ||||
|  | ||||
| @ -1,9 +1,13 @@ | ||||
| """http helpers""" | ||||
| from os import environ | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.http import HttpRequest | ||||
| from requests.sessions import Session | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | ||||
|  | ||||
| OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | ||||
| OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec | ||||
| DEFAULT_IP = "255.255.255.255" | ||||
| @ -60,3 +64,16 @@ def get_client_ip(request: Optional[HttpRequest]) -> str: | ||||
|     if override: | ||||
|         return override | ||||
|     return _get_client_ip_from_meta(request.META) | ||||
|  | ||||
|  | ||||
| def authentik_user_agent() -> str: | ||||
|     """Get a common user agent""" | ||||
|     build = environ.get(ENV_GIT_HASH_KEY, "tagged") | ||||
|     return f"authentik@{__version__} (build={build})" | ||||
|  | ||||
|  | ||||
| def get_http_session() -> Session: | ||||
|     """Get a requests session with common headers""" | ||||
|     session = Session() | ||||
|     session.headers["User-Agent"] = authentik_user_agent() | ||||
|     return session | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| """authentik lib reflection utilities""" | ||||
| from importlib import import_module | ||||
| from typing import Union | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
| @ -19,12 +20,12 @@ def all_subclasses(cls, sort=True): | ||||
|     return classes | ||||
|  | ||||
|  | ||||
| def class_to_path(cls): | ||||
| def class_to_path(cls: type) -> str: | ||||
|     """Turn Class (Class or instance) into module path""" | ||||
|     return f"{cls.__module__}.{cls.__name__}" | ||||
|  | ||||
|  | ||||
| def path_to_class(path): | ||||
| def path_to_class(path: Union[str, None]) -> Union[type, None]: | ||||
|     """Import module and return class""" | ||||
|     if not path: | ||||
|         return None | ||||
|  | ||||
| @ -15,7 +15,7 @@ from authentik.core.channels import AuthJsonConsumer | ||||
| from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState | ||||
|  | ||||
| GAUGE_OUTPOSTS_CONNECTED = Gauge( | ||||
|     "authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"] | ||||
|     "authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid", "expected"] | ||||
| ) | ||||
| GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( | ||||
|     "authentik_outposts_last_update", | ||||
| @ -76,6 +76,7 @@ class OutpostConsumer(AuthJsonConsumer): | ||||
|             GAUGE_OUTPOSTS_CONNECTED.labels( | ||||
|                 outpost=self.outpost.name, | ||||
|                 uid=self.last_uid, | ||||
|                 expected=self.outpost.config.kubernetes_replicas, | ||||
|             ).dec() | ||||
|         LOGGER.debug( | ||||
|             "removed outpost instance from cache", | ||||
| @ -100,9 +101,10 @@ class OutpostConsumer(AuthJsonConsumer): | ||||
|             GAUGE_OUTPOSTS_CONNECTED.labels( | ||||
|                 outpost=self.outpost.name, | ||||
|                 uid=self.last_uid, | ||||
|                 expected=self.outpost.config.kubernetes_replicas, | ||||
|             ).inc() | ||||
|             LOGGER.debug( | ||||
|                 "added outpost instace to cache", | ||||
|                 "added outpost instance to cache", | ||||
|                 outpost=self.outpost, | ||||
|                 instance_uuid=self.last_uid, | ||||
|             ) | ||||
|  | ||||
| @ -38,6 +38,7 @@ class DockerController(BaseController): | ||||
|             "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(), | ||||
|             "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(), | ||||
|             "AUTHENTIK_TOKEN": self.outpost.token.key, | ||||
|             "AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser, | ||||
|         } | ||||
|  | ||||
|     def _comp_env(self, container: Container) -> bool: | ||||
| @ -75,6 +76,9 @@ class DockerController(BaseController): | ||||
|         #   {'HostIp': '0.0.0.0', 'HostPort': '389'}, | ||||
|         #   {'HostIp': '::', 'HostPort': '389'} | ||||
|         # ]} | ||||
|         # If no ports are mapped (either mapping disabled, or host network) | ||||
|         if not container.ports: | ||||
|             return False | ||||
|         for port in self.deployment_ports: | ||||
|             key = f"{port.inner_port or port.port}/{port.protocol.lower()}" | ||||
|             if key not in container.ports: | ||||
| @ -98,15 +102,16 @@ class DockerController(BaseController): | ||||
|                 "image": image_name, | ||||
|                 "name": container_name, | ||||
|                 "detach": True, | ||||
|                 "ports": { | ||||
|                     f"{port.inner_port or port.port}/{port.protocol.lower()}": port.port | ||||
|                     for port in self.deployment_ports | ||||
|                 }, | ||||
|                 "environment": self._get_env(), | ||||
|                 "labels": self._get_labels(), | ||||
|                 "restart_policy": {"Name": "unless-stopped"}, | ||||
|                 "network": self.outpost.config.docker_network, | ||||
|             } | ||||
|             if self.outpost.config.docker_map_ports: | ||||
|                 container_args["ports"] = { | ||||
|                     f"{port.inner_port or port.port}/{port.protocol.lower()}": str(port.port) | ||||
|                     for port in self.deployment_ports | ||||
|                 } | ||||
|             if settings.TEST: | ||||
|                 del container_args["ports"] | ||||
|                 del container_args["network"] | ||||
| @ -164,11 +169,9 @@ class DockerController(BaseController): | ||||
|                 self.down() | ||||
|                 return self.up(depth + 1) | ||||
|             # Check that container is healthy | ||||
|             if ( | ||||
|                 container.status == "running" | ||||
|                 and container.attrs.get("State", {}).get("Health", {}).get("Status", "") | ||||
|                 != "healthy" | ||||
|             ): | ||||
|             if container.status == "running" and container.attrs.get("State", {}).get( | ||||
|                 "Health", {} | ||||
|             ).get("Status", "") not in ["healthy", "starting"]: | ||||
|                 # At this point we know the config is correct, but the container isn't healthy, | ||||
|                 # so we just restart it with the same config | ||||
|                 if has_been_created: | ||||
| @ -217,6 +220,7 @@ class DockerController(BaseController): | ||||
|                         "AUTHENTIK_HOST": self.outpost.config.authentik_host, | ||||
|                         "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure), | ||||
|                         "AUTHENTIK_TOKEN": self.outpost.token.key, | ||||
|                         "AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser, | ||||
|                     }, | ||||
|                     "labels": self._get_labels(), | ||||
|                 } | ||||
|  | ||||
| @ -10,7 +10,7 @@ from structlog.stdlib import get_logger | ||||
| from urllib3.exceptions import HTTPError | ||||
|  | ||||
| from authentik import __version__ | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate | ||||
| from authentik.outposts.managed import MANAGED_OUTPOST | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @ -20,18 +20,6 @@ if TYPE_CHECKING: | ||||
| T = TypeVar("T", V1Pod, V1Deployment) | ||||
|  | ||||
|  | ||||
| class ReconcileTrigger(SentryIgnoredException): | ||||
|     """Base trigger raised by child classes to notify us""" | ||||
|  | ||||
|  | ||||
| class NeedsRecreate(ReconcileTrigger): | ||||
|     """Exception to trigger a complete recreate of the Kubernetes Object""" | ||||
|  | ||||
|  | ||||
| class NeedsUpdate(ReconcileTrigger): | ||||
|     """Exception to trigger an update to the Kubernetes Object""" | ||||
|  | ||||
|  | ||||
| class KubernetesObjectReconciler(Generic[T]): | ||||
|     """Base Kubernetes Reconciler, handles the basic logic.""" | ||||
|  | ||||
| @ -109,7 +97,7 @@ class KubernetesObjectReconciler(Generic[T]): | ||||
|         except (OpenApiException, HTTPError) as exc: | ||||
|             # pylint: disable=no-member | ||||
|             if isinstance(exc, ApiException) and exc.status == 404: | ||||
|                 self.logger.debug("Failed to get current, assuming non-existant") | ||||
|                 self.logger.debug("Failed to get current, assuming non-existent") | ||||
|                 return | ||||
|             self.logger.debug("Other unhandled error", exc=exc) | ||||
|             raise exc | ||||
| @ -129,7 +117,7 @@ class KubernetesObjectReconciler(Generic[T]): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def retrieve(self) -> T: | ||||
|         """API Wrapper to retrive object""" | ||||
|         """API Wrapper to retrieve object""" | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def delete(self, reference: T): | ||||
|  | ||||
| @ -17,7 +17,9 @@ from kubernetes.client import ( | ||||
| ) | ||||
|  | ||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | ||||
| from authentik.outposts.controllers.k8s.triggers import NeedsUpdate | ||||
| from authentik.outposts.controllers.k8s.utils import compare_ports | ||||
| from authentik.outposts.models import Outpost | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @ -35,7 +37,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | ||||
|         self.outpost = self.controller.outpost | ||||
|  | ||||
|     def reconcile(self, current: V1Deployment, reference: V1Deployment): | ||||
|         super().reconcile(current, reference) | ||||
|         compare_ports( | ||||
|             current.spec.template.spec.containers[0].ports, | ||||
|             reference.spec.template.spec.containers[0].ports, | ||||
|         ) | ||||
|         if current.spec.replicas != reference.spec.replicas: | ||||
|             raise NeedsUpdate() | ||||
|         if ( | ||||
| @ -43,6 +48,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | ||||
|             != reference.spec.template.spec.containers[0].image | ||||
|         ): | ||||
|             raise NeedsUpdate() | ||||
|         super().reconcile(current, reference) | ||||
|  | ||||
|     def get_pod_meta(self) -> dict[str, str]: | ||||
|         """Get common object metadata""" | ||||
| @ -89,6 +95,15 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | ||||
|                                             ) | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                     V1EnvVar( | ||||
|                                         name="AUTHENTIK_HOST_BROWSER", | ||||
|                                         value_from=V1EnvVarSource( | ||||
|                                             secret_key_ref=V1SecretKeySelector( | ||||
|                                                 name=self.name, | ||||
|                                                 key="authentik_host_browser", | ||||
|                                             ) | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                     V1EnvVar( | ||||
|                                         name="AUTHENTIK_TOKEN", | ||||
|                                         value_from=V1EnvVarSource( | ||||
|  | ||||
| @ -5,7 +5,8 @@ from typing import TYPE_CHECKING | ||||
| from kubernetes.client import CoreV1Api, V1Secret | ||||
|  | ||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | ||||
| from authentik.outposts.controllers.k8s.triggers import NeedsUpdate | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from authentik.outposts.controllers.kubernetes import KubernetesController | ||||
| @ -26,7 +27,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]): | ||||
|     def reconcile(self, current: V1Secret, reference: V1Secret): | ||||
|         super().reconcile(current, reference) | ||||
|         for key in reference.data.keys(): | ||||
|             if current.data[key] != reference.data[key]: | ||||
|             if key not in current.data or current.data[key] != reference.data[key]: | ||||
|                 raise NeedsUpdate() | ||||
|  | ||||
|     def get_reference_object(self) -> V1Secret: | ||||
| @ -40,6 +41,9 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]): | ||||
|                     str(self.controller.outpost.config.authentik_host_insecure) | ||||
|                 ), | ||||
|                 "token": b64string(self.controller.outpost.token.key), | ||||
|                 "authentik_host_browser": b64string( | ||||
|                     self.controller.outpost.config.authentik_host_browser | ||||
|                 ), | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -3,9 +3,10 @@ from typing import TYPE_CHECKING | ||||
|  | ||||
| from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec | ||||
|  | ||||
| from authentik.outposts.controllers.base import FIELD_MANAGER, DeploymentPort | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate | ||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | ||||
| from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler | ||||
| from authentik.outposts.controllers.k8s.utils import compare_ports | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from authentik.outposts.controllers.kubernetes import KubernetesController | ||||
| @ -19,46 +20,15 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]): | ||||
|         self.api = CoreV1Api(controller.client) | ||||
|  | ||||
|     def reconcile(self, current: V1Service, reference: V1Service): | ||||
|         compare_ports(current.spec.ports, reference.spec.ports) | ||||
|         # run the base reconcile last, as that will probably raise NeedsUpdate | ||||
|         # after an authentik update. However the ports might have also changed during | ||||
|         # the update, so this causes the service to be re-created with higher | ||||
|         # priority than being updated. | ||||
|         super().reconcile(current, reference) | ||||
|         if len(current.spec.ports) != len(reference.spec.ports): | ||||
|             raise NeedsUpdate() | ||||
|         for port in reference.spec.ports: | ||||
|             if port not in current.spec.ports: | ||||
|                 raise NeedsUpdate() | ||||
|  | ||||
|     def get_embedded_reference_object(self) -> V1Service: | ||||
|         """Get Service for embedded outpost""" | ||||
|         selector_labels = { | ||||
|             "app.kubernetes.io/name": "authentik", | ||||
|             "app.kubernetes.io/component": "server", | ||||
|         } | ||||
|         meta = self.get_object_meta(name=self.name) | ||||
|         ports = [] | ||||
|         for port in [ | ||||
|             DeploymentPort(9000, "http", "tcp"), | ||||
|             DeploymentPort(9443, "https", "tcp"), | ||||
|         ]: | ||||
|             ports.append( | ||||
|                 V1ServicePort( | ||||
|                     name=port.name, | ||||
|                     port=port.port, | ||||
|                     protocol=port.protocol.upper(), | ||||
|                     target_port=port.inner_port or port.port, | ||||
|                 ) | ||||
|             ) | ||||
|         return V1Service( | ||||
|             metadata=meta, | ||||
|             spec=V1ServiceSpec( | ||||
|                 ports=ports, | ||||
|                 selector=selector_labels, | ||||
|                 type=self.controller.outpost.config.kubernetes_service_type, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     def get_reference_object(self) -> V1Service: | ||||
|         """Get deployment object for outpost""" | ||||
|         if self.is_embedded: | ||||
|             return self.get_embedded_reference_object() | ||||
|         meta = self.get_object_meta(name=self.name) | ||||
|         ports = [] | ||||
|         for port in self.controller.deployment_ports: | ||||
| @ -70,7 +40,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]): | ||||
|                     target_port=port.inner_port or port.port, | ||||
|                 ) | ||||
|             ) | ||||
|         selector_labels = DeploymentReconciler(self.controller).get_pod_meta() | ||||
|         if self.is_embedded: | ||||
|             selector_labels = { | ||||
|                 "app.kubernetes.io/name": "authentik", | ||||
|                 "app.kubernetes.io/component": "server", | ||||
|             } | ||||
|         else: | ||||
|             selector_labels = DeploymentReconciler(self.controller).get_pod_meta() | ||||
|         return V1Service( | ||||
|             metadata=meta, | ||||
|             spec=V1ServiceSpec( | ||||
|  | ||||
							
								
								
									
										150
									
								
								authentik/outposts/controllers/k8s/service_monitor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								authentik/outposts/controllers/k8s/service_monitor.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| """Kubernetes Prometheus ServiceMonitor Reconciler""" | ||||
| from dataclasses import asdict, dataclass, field | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from dacite import from_dict | ||||
| from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi | ||||
|  | ||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | ||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from authentik.outposts.controllers.kubernetes import KubernetesController | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class PrometheusServiceMonitorSpecEndpoint: | ||||
|     """Prometheus ServiceMonitor endpoint spec""" | ||||
|  | ||||
|     port: str | ||||
|     path: str = field(default="/metrics") | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class PrometheusServiceMonitorSpecSelector: | ||||
|     """Prometheus ServiceMonitor selector spec""" | ||||
|  | ||||
|     # pylint: disable=invalid-name | ||||
|     matchLabels: dict | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class PrometheusServiceMonitorSpec: | ||||
|     """Prometheus ServiceMonitor spec""" | ||||
|  | ||||
|     endpoints: list[PrometheusServiceMonitorSpecEndpoint] | ||||
|     # pylint: disable=invalid-name | ||||
|     selector: PrometheusServiceMonitorSpecSelector | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class PrometheusServiceMonitorMetadata: | ||||
|     """Prometheus ServiceMonitor metadata""" | ||||
|  | ||||
|     name: str | ||||
|     namespace: str | ||||
|     labels: dict = field(default_factory=dict) | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class PrometheusServiceMonitor: | ||||
|     """Prometheus ServiceMonitor""" | ||||
|  | ||||
|     # pylint: disable=invalid-name | ||||
|     apiVersion: str | ||||
|     kind: str | ||||
|     metadata: PrometheusServiceMonitorMetadata | ||||
|     spec: PrometheusServiceMonitorSpec | ||||
|  | ||||
|  | ||||
| CRD_NAME = "servicemonitors.monitoring.coreos.com" | ||||
| CRD_GROUP = "monitoring.coreos.com" | ||||
| CRD_VERSION = "v1" | ||||
| CRD_PLURAL = "servicemonitors" | ||||
|  | ||||
|  | ||||
| class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusServiceMonitor]): | ||||
|     """Kubernetes Prometheus ServiceMonitor Reconciler""" | ||||
|  | ||||
|     def __init__(self, controller: "KubernetesController") -> None: | ||||
|         super().__init__(controller) | ||||
|         self.api_ex = ApiextensionsV1Api(controller.client) | ||||
|         self.api = CustomObjectsApi(controller.client) | ||||
|  | ||||
|     @property | ||||
|     def noop(self) -> bool: | ||||
|         return (not self._crd_exists()) or (self.is_embedded) | ||||
|  | ||||
|     def _crd_exists(self) -> bool: | ||||
|         """Check if the Prometheus ServiceMonitor exists""" | ||||
|         return bool( | ||||
|             len( | ||||
|                 self.api_ex.list_custom_resource_definition( | ||||
|                     field_selector=f"metadata.name={CRD_NAME}" | ||||
|                 ).items | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     def get_reference_object(self) -> PrometheusServiceMonitor: | ||||
|         """Get service monitor object for outpost""" | ||||
|         return PrometheusServiceMonitor( | ||||
|             apiVersion=f"{CRD_GROUP}/{CRD_VERSION}", | ||||
|             kind="ServiceMonitor", | ||||
|             metadata=PrometheusServiceMonitorMetadata( | ||||
|                 name=self.name, | ||||
|                 namespace=self.namespace, | ||||
|                 labels=self.get_object_meta().labels, | ||||
|             ), | ||||
|             spec=PrometheusServiceMonitorSpec( | ||||
|                 endpoints=[ | ||||
|                     PrometheusServiceMonitorSpecEndpoint( | ||||
|                         port="http-metrics", | ||||
|                     ) | ||||
|                 ], | ||||
|                 selector=PrometheusServiceMonitorSpecSelector( | ||||
|                     matchLabels=self.get_object_meta(name=self.name).labels, | ||||
|                 ), | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     def create(self, reference: PrometheusServiceMonitor): | ||||
|         return self.api.create_namespaced_custom_object( | ||||
|             group=CRD_GROUP, | ||||
|             version=CRD_VERSION, | ||||
|             plural=CRD_PLURAL, | ||||
|             namespace=self.namespace, | ||||
|             body=asdict(reference), | ||||
|             field_manager=FIELD_MANAGER, | ||||
|         ) | ||||
|  | ||||
|     def delete(self, reference: PrometheusServiceMonitor): | ||||
|         return self.api.delete_namespaced_custom_object( | ||||
|             group=CRD_GROUP, | ||||
|             version=CRD_VERSION, | ||||
|             namespace=self.namespace, | ||||
|             plural=CRD_PLURAL, | ||||
|             name=self.name, | ||||
|         ) | ||||
|  | ||||
|     def retrieve(self) -> PrometheusServiceMonitor: | ||||
|         return from_dict( | ||||
|             PrometheusServiceMonitor, | ||||
|             self.api.get_namespaced_custom_object( | ||||
|                 group=CRD_GROUP, | ||||
|                 version=CRD_VERSION, | ||||
|                 namespace=self.namespace, | ||||
|                 plural=CRD_PLURAL, | ||||
|                 name=self.name, | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     def update(self, current: PrometheusServiceMonitor, reference: PrometheusServiceMonitor): | ||||
|         return self.api.patch_namespaced_custom_object( | ||||
|             group=CRD_GROUP, | ||||
|             version=CRD_VERSION, | ||||
|             namespace=self.namespace, | ||||
|             plural=CRD_PLURAL, | ||||
|             name=self.name, | ||||
|             body=asdict(reference), | ||||
|             field_manager=FIELD_MANAGER, | ||||
|         ) | ||||
							
								
								
									
										14
									
								
								authentik/outposts/controllers/k8s/triggers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/outposts/controllers/k8s/triggers.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| """exceptions used by the kubernetes reconciler to trigger updates""" | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
|  | ||||
|  | ||||
| class ReconcileTrigger(SentryIgnoredException): | ||||
|     """Base trigger raised by child classes to notify us""" | ||||
|  | ||||
|  | ||||
| class NeedsRecreate(ReconcileTrigger): | ||||
|     """Exception to trigger a complete recreate of the Kubernetes Object""" | ||||
|  | ||||
|  | ||||
| class NeedsUpdate(ReconcileTrigger): | ||||
|     """Exception to trigger an update to the Kubernetes Object""" | ||||
| @ -1,8 +1,11 @@ | ||||
| """k8s utils""" | ||||
| from pathlib import Path | ||||
|  | ||||
| from kubernetes.client.models.v1_container_port import V1ContainerPort | ||||
| from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME | ||||
|  | ||||
| from authentik.outposts.controllers.k8s.triggers import NeedsRecreate | ||||
|  | ||||
|  | ||||
| def get_namespace() -> str: | ||||
|     """Get the namespace if we're running in a pod, otherwise default to default""" | ||||
| @ -11,3 +14,12 @@ def get_namespace() -> str: | ||||
|         with open(path, "r", encoding="utf8") as _namespace_file: | ||||
|             return _namespace_file.read() | ||||
|     return "default" | ||||
|  | ||||
|  | ||||
| def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]): | ||||
|     """Compare ports of a list""" | ||||
|     if len(current) != len(reference): | ||||
|         raise NeedsRecreate() | ||||
|     for port in reference: | ||||
|         if port not in current: | ||||
|             raise NeedsRecreate() | ||||
|  | ||||
| @ -13,6 +13,7 @@ from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | ||||
| from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler | ||||
| from authentik.outposts.controllers.k8s.secret import SecretReconciler | ||||
| from authentik.outposts.controllers.k8s.service import ServiceReconciler | ||||
| from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler | ||||
| from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid | ||||
|  | ||||
|  | ||||
| @ -32,8 +33,9 @@ class KubernetesController(BaseController): | ||||
|             "secret": SecretReconciler, | ||||
|             "deployment": DeploymentReconciler, | ||||
|             "service": ServiceReconciler, | ||||
|             "prometheus servicemonitor": PrometheusServiceMonitorReconciler, | ||||
|         } | ||||
|         self.reconcile_order = ["secret", "deployment", "service"] | ||||
|         self.reconcile_order = ["secret", "deployment", "service", "prometheus servicemonitor"] | ||||
|  | ||||
|     def up(self): | ||||
|         try: | ||||
|  | ||||
| @ -30,7 +30,7 @@ class DockerInlineTLS: | ||||
|         return str(path) | ||||
|  | ||||
|     def write(self) -> TLSConfig: | ||||
|         """Create TLSConfig with Certificate Keypairs""" | ||||
|         """Create TLSConfig with Certificate Key pairs""" | ||||
|         # So yes, this is quite ugly. But sadly, there is no clean way to pass | ||||
|         # docker-py (which is using requests (which is using urllib3)) a certificate | ||||
|         # for verification or authentication as string. | ||||
|  | ||||
| @ -64,6 +64,7 @@ class OutpostConfig: | ||||
|  | ||||
|     authentik_host: str = "" | ||||
|     authentik_host_insecure: bool = False | ||||
|     authentik_host_browser: str = "" | ||||
|  | ||||
|     log_level: str = CONFIG.y("log_level") | ||||
|     error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled") | ||||
| @ -71,6 +72,7 @@ class OutpostConfig: | ||||
|     object_naming_template: str = field(default="ak-outpost-%(name)s") | ||||
|  | ||||
|     docker_network: Optional[str] = field(default=None) | ||||
|     docker_map_ports: bool = field(default=True) | ||||
|  | ||||
|     kubernetes_replicas: int = field(default=1) | ||||
|     kubernetes_namespace: str = field(default_factory=get_namespace) | ||||
| @ -339,19 +341,8 @@ class Outpost(ManagedModel): | ||||
|         """Username for service user""" | ||||
|         return f"ak-outpost-{self.uuid.hex}" | ||||
|  | ||||
|     @property | ||||
|     def user(self) -> User: | ||||
|         """Get/create user with access to all required objects""" | ||||
|         users = User.objects.filter(username=self.user_identifier) | ||||
|         if not users.exists(): | ||||
|             user: User = User.objects.create(username=self.user_identifier) | ||||
|             user.set_unusable_password() | ||||
|             user.save() | ||||
|         else: | ||||
|             user = users.first() | ||||
|         user.attributes[USER_ATTRIBUTE_SA] = True | ||||
|         user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True | ||||
|         user.save() | ||||
|     def build_user_permissions(self, user: User): | ||||
|         """Create per-object and global permissions for outpost service-account""" | ||||
|         # To ensure the user only has the correct permissions, we delete all of them and re-add | ||||
|         # the ones the user needs | ||||
|         with transaction.atomic(): | ||||
| @ -395,6 +386,23 @@ class Outpost(ManagedModel): | ||||
|             "Updated service account's permissions", | ||||
|             perms=UserObjectPermission.objects.filter(user=user), | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def user(self) -> User: | ||||
|         """Get/create user with access to all required objects""" | ||||
|         users = User.objects.filter(username=self.user_identifier) | ||||
|         should_create_user = not users.exists() | ||||
|         if should_create_user: | ||||
|             user: User = User.objects.create(username=self.user_identifier) | ||||
|             user.set_unusable_password() | ||||
|             user.save() | ||||
|         else: | ||||
|             user = users.first() | ||||
|         user.attributes[USER_ATTRIBUTE_SA] = True | ||||
|         user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True | ||||
|         user.save() | ||||
|         if should_create_user: | ||||
|             self.build_user_permissions(user) | ||||
|         return user | ||||
|  | ||||
|     @property | ||||
|  | ||||
| @ -100,7 +100,7 @@ def outpost_controller( | ||||
|     if from_cache: | ||||
|         outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk) | ||||
|     else: | ||||
|         outpost: Outpost = Outpost.objects.get(pk=outpost_pk) | ||||
|         outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first() | ||||
|     if not outpost: | ||||
|         return | ||||
|     self.set_uid(slugify(outpost.name)) | ||||
| @ -126,6 +126,7 @@ def outpost_token_ensurer(self: MonitoredTask): | ||||
|     all_outposts = Outpost.objects.all() | ||||
|     for outpost in all_outposts: | ||||
|         _ = outpost.token | ||||
|         outpost.build_user_permissions(outpost.user) | ||||
|     self.set_status( | ||||
|         TaskResult( | ||||
|             TaskResultStatus.SUCCESSFUL, | ||||
| @ -148,10 +149,7 @@ def outpost_post_save(model_class: str, model_pk: Any): | ||||
|         return | ||||
|  | ||||
|     if isinstance(instance, Outpost): | ||||
|         LOGGER.debug("Ensuring token and permissions for outpost", instance=instance) | ||||
|         _ = instance.token | ||||
|         _ = instance.user | ||||
|         LOGGER.debug("Trigger reconcile for outpost") | ||||
|         LOGGER.debug("Trigger reconcile for outpost", instance=instance) | ||||
|         outpost_controller.delay(instance.pk) | ||||
|  | ||||
|     if isinstance(instance, (OutpostModel, Outpost)): | ||||
| @ -184,7 +182,7 @@ def outpost_post_save(model_class: str, model_pk: Any): | ||||
|  | ||||
|  | ||||
| def outpost_send_update(model_instace: Model): | ||||
|     """Send outpost update to all registered outposts, irregardless to which authentik | ||||
|     """Send outpost update to all registered outposts, regardless to which authentik | ||||
|     instance they are connected""" | ||||
|     channel_layer = get_channel_layer() | ||||
|     if isinstance(model_instace, OutpostModel): | ||||
| @ -199,7 +197,7 @@ def _outpost_single_update(outpost: Outpost, layer=None): | ||||
|     # Ensure token again, because this function is called when anything related to an | ||||
|     # OutpostModel is saved, so we can be sure permissions are right | ||||
|     _ = outpost.token | ||||
|     _ = outpost.user | ||||
|     outpost.build_user_permissions(outpost.user) | ||||
|     if not layer:  # pragma: no cover | ||||
|         layer = get_channel_layer() | ||||
|     for state in OutpostState.for_outpost(outpost): | ||||
| @ -211,7 +209,7 @@ def _outpost_single_update(outpost: Outpost, layer=None): | ||||
| @CELERY_APP.task() | ||||
| def outpost_local_connection(): | ||||
|     """Checks the local environment and create Service connections.""" | ||||
|     # Explicitly check against token filename, as thats | ||||
|     # Explicitly check against token filename, as that's | ||||
|     # only present when the integration is enabled | ||||
|     if Path(SERVICE_TOKEN_FILENAME).exists(): | ||||
|         LOGGER.debug("Detected in-cluster Kubernetes Config") | ||||
|  | ||||
| @ -87,6 +87,7 @@ class PolicyViewSet( | ||||
|         "promptstage": ["isnull"], | ||||
|     } | ||||
|     search_fields = ["name"] | ||||
|     ordering = ["name"] | ||||
|  | ||||
|     def get_queryset(self):  # pragma: no cover | ||||
|         return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set") | ||||
|  | ||||
| @ -81,11 +81,11 @@ class PolicyEngine: | ||||
|             .iterator() | ||||
|         ) | ||||
|  | ||||
|     def _check_policy_type(self, policy: Policy): | ||||
|     def _check_policy_type(self, binding: PolicyBinding): | ||||
|         """Check policy type, make sure it's not the root class as that has no logic implemented""" | ||||
|         # pyright: reportGeneralTypeIssues=false | ||||
|         if policy.__class__ == Policy: | ||||
|             raise TypeError(f"Policy '{policy}' is root type") | ||||
|         if binding.policy is not None and binding.policy.__class__ == Policy: | ||||
|             raise TypeError(f"Policy '{binding.policy}' is root type") | ||||
|  | ||||
|     def build(self) -> "PolicyEngine": | ||||
|         """Build wrapper which monitors performance""" | ||||
| @ -102,7 +102,7 @@ class PolicyEngine: | ||||
|             for binding in self._iter_bindings(): | ||||
|                 self.__expected_result_count += 1 | ||||
|  | ||||
|                 self._check_policy_type(binding.policy) | ||||
|                 self._check_policy_type(binding) | ||||
|                 key = cache_key(binding, self.request) | ||||
|                 cached_policy = cache.get(key, None) | ||||
|                 if cached_policy and self.use_cache: | ||||
|  | ||||
| @ -3,8 +3,10 @@ from ipaddress import ip_address, ip_network | ||||
| from typing import TYPE_CHECKING, Optional | ||||
|  | ||||
| from django.http import HttpRequest | ||||
| from django_otp import devices_for_user | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.flows.planner import PLAN_CONTEXT_SSO | ||||
| from authentik.lib.expression.evaluator import BaseEvaluator | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
| @ -28,6 +30,7 @@ class PolicyEvaluator(BaseEvaluator): | ||||
|         self._messages = [] | ||||
|         self._context["ak_logger"] = get_logger(policy_name) | ||||
|         self._context["ak_message"] = self.expr_func_message | ||||
|         self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator | ||||
|         self._context["ip_address"] = ip_address | ||||
|         self._context["ip_network"] = ip_network | ||||
|         self._filename = policy_name or "PolicyEvaluator" | ||||
| @ -36,6 +39,19 @@ class PolicyEvaluator(BaseEvaluator): | ||||
|         """Wrapper to append to messages list, which is returned with PolicyResult""" | ||||
|         self._messages.append(message) | ||||
|  | ||||
|     def expr_func_user_has_authenticator( | ||||
|         self, user: User, device_type: Optional[str] = None | ||||
|     ) -> bool: | ||||
|         """Check if a user has any authenticator devices, optionally matching *device_type*""" | ||||
|         user_devices = devices_for_user(user) | ||||
|         if device_type: | ||||
|             for device in user_devices: | ||||
|                 device_class = device.__class__.__name__.lower().replace("device", "") | ||||
|                 if device_class == device_type: | ||||
|                     return True | ||||
|             return False | ||||
|         return len(user_devices) > 0 | ||||
|  | ||||
|     def set_policy_request(self, request: PolicyRequest): | ||||
|         """Update context based on policy request (if http request is given, update that too)""" | ||||
|         # update website/docs/expressions/_objects.md | ||||
|  | ||||
| @ -3,10 +3,10 @@ from hashlib import sha1 | ||||
|  | ||||
| from django.db import models | ||||
| from django.utils.translation import gettext as _ | ||||
| from requests import get | ||||
| from rest_framework.serializers import BaseSerializer | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.lib.utils.http import get_http_session | ||||
| from authentik.policies.models import Policy, PolicyResult | ||||
| from authentik.policies.types import PolicyRequest | ||||
|  | ||||
| @ -49,7 +49,7 @@ class HaveIBeenPwendPolicy(Policy): | ||||
|  | ||||
|         pw_hash = sha1(password.encode("utf-8")).hexdigest()  # nosec | ||||
|         url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}" | ||||
|         result = get(url).text | ||||
|         result = get_http_session().get(url).text | ||||
|         final_count = 0 | ||||
|         for line in result.split("\r\n"): | ||||
|             full_hash, count = line.split(":") | ||||
|  | ||||
| @ -59,19 +59,22 @@ class PasswordPolicy(Policy): | ||||
|             password = request.context[PLAN_CONTEXT_PROMPT][self.password_field] | ||||
|  | ||||
|         if len(password) < self.length_min: | ||||
|             LOGGER.debug("password failed", reason="length", p=password) | ||||
|             LOGGER.debug("password failed", reason="length") | ||||
|             return PolicyResult(False, self.error_message) | ||||
|  | ||||
|         if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase: | ||||
|             LOGGER.debug("password failed", reason="amount_lowercase", p=password) | ||||
|             LOGGER.debug("password failed", reason="amount_lowercase") | ||||
|             return PolicyResult(False, self.error_message) | ||||
|         if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase: | ||||
|             LOGGER.debug("password failed", reason="amount_uppercase", p=password) | ||||
|             return PolicyResult(False, self.error_message) | ||||
|         regex = re.compile(r"[%s]" % self.symbol_charset) | ||||
|         if self.amount_symbols > 0 and len(regex.findall(password)) < self.amount_symbols: | ||||
|             LOGGER.debug("password failed", reason="amount_symbols", p=password) | ||||
|             LOGGER.debug("password failed", reason="amount_uppercase") | ||||
|             return PolicyResult(False, self.error_message) | ||||
|         if self.amount_symbols > 0: | ||||
|             count = 0 | ||||
|             for symbol in self.symbol_charset: | ||||
|                 count += password.count(symbol) | ||||
|             if count < self.amount_symbols: | ||||
|                 LOGGER.debug("password failed", reason="amount_symbols") | ||||
|                 return PolicyResult(False, self.error_message) | ||||
|  | ||||
|         return PolicyResult(True) | ||||
|  | ||||
|  | ||||
| @ -46,7 +46,7 @@ def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str: | ||||
|  | ||||
|  | ||||
| class PolicyProcess(PROCESS_CLASS): | ||||
|     """Evaluate a single policy within a seprate process""" | ||||
|     """Evaluate a single policy within a separate process""" | ||||
|  | ||||
|     connection: Connection | ||||
|     binding: PolicyBinding | ||||
|  | ||||
| @ -34,7 +34,7 @@ def update_score(request: HttpRequest, username: str, amount: int): | ||||
| @receiver(user_login_failed) | ||||
| # pylint: disable=unused-argument | ||||
| def handle_failed_login(sender, request, credentials, **_): | ||||
|     """Lower Score for failed loging attempts""" | ||||
|     """Lower Score for failed login attempts""" | ||||
|     if "username" in credentials: | ||||
|         update_score(request, credentials.get("username"), -1) | ||||
|  | ||||
|  | ||||
| @ -14,7 +14,7 @@ from authentik.policies.types import PolicyRequest | ||||
|  | ||||
|  | ||||
| def clear_policy_cache(): | ||||
|     """Ensure no policy-related keys are stil cached""" | ||||
|     """Ensure no policy-related keys are still cached""" | ||||
|     keys = cache.keys("policy_*") | ||||
|     cache.delete(keys) | ||||
|  | ||||
|  | ||||
| @ -29,7 +29,19 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet): | ||||
|  | ||||
|     queryset = LDAPProvider.objects.all() | ||||
|     serializer_class = LDAPProviderSerializer | ||||
|     filterset_fields = "__all__" | ||||
|     filterset_fields = { | ||||
|         "application": ["isnull"], | ||||
|         "name": ["iexact"], | ||||
|         "authorization_flow__slug": ["iexact"], | ||||
|         "base_dn": ["iexact"], | ||||
|         "search_group__group_uuid": ["iexact"], | ||||
|         "search_group__name": ["iexact"], | ||||
|         "certificate__kp_uuid": ["iexact"], | ||||
|         "certificate__name": ["iexact"], | ||||
|         "tls_server_name": ["iexact"], | ||||
|         "uid_start_number": ["iexact"], | ||||
|         "gid_start_number": ["iexact"], | ||||
|     } | ||||
|     ordering = ["name"] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| """LDAP Provider Docker Contoller""" | ||||
| """LDAP Provider Docker Controller""" | ||||
| from authentik.outposts.controllers.base import DeploymentPort | ||||
| from authentik.outposts.controllers.docker import DockerController | ||||
| from authentik.outposts.models import DockerServiceConnection, Outpost | ||||
|  | ||||
|  | ||||
| class LDAPDockerController(DockerController): | ||||
|     """LDAP Provider Docker Contoller""" | ||||
|     """LDAP Provider Docker Controller""" | ||||
|  | ||||
|     def __init__(self, outpost: Outpost, connection: DockerServiceConnection): | ||||
|         super().__init__(outpost, connection) | ||||
|  | ||||
| @ -1,15 +1,16 @@ | ||||
| """LDAP Provider Kubernetes Contoller""" | ||||
| """LDAP Provider Kubernetes Controller""" | ||||
| from authentik.outposts.controllers.base import DeploymentPort | ||||
| from authentik.outposts.controllers.kubernetes import KubernetesController | ||||
| from authentik.outposts.models import KubernetesServiceConnection, Outpost | ||||
|  | ||||
|  | ||||
| class LDAPKubernetesController(KubernetesController): | ||||
|     """LDAP Provider Kubernetes Contoller""" | ||||
|     """LDAP Provider Kubernetes Controller""" | ||||
|  | ||||
|     def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection): | ||||
|         super().__init__(outpost, connection) | ||||
|         self.deployment_ports = [ | ||||
|             DeploymentPort(389, "ldap", "tcp", 3389), | ||||
|             DeploymentPort(636, "ldaps", "tcp", 6636), | ||||
|             DeploymentPort(9300, "http-metrics", "tcp", 9300), | ||||
|         ] | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| """OAuth2Provider API Views""" | ||||
| from django_filters.filters import AllValuesMultipleFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import extend_schema_field | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||
| @ -23,7 +25,7 @@ class ScopeMappingSerializer(PropertyMappingSerializer): | ||||
| class ScopeMappingFilter(FilterSet): | ||||
|     """Filter for ScopeMapping""" | ||||
|  | ||||
|     managed = AllValuesMultipleFilter(field_name="managed") | ||||
|     managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed")) | ||||
|  | ||||
|     class Meta: | ||||
|         model = ScopeMapping | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	