Compare commits
	
		
			301 Commits
		
	
	
		
			version-20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ffb0135f06 | |||
| ee0ddc3d17 | |||
| 5dd979d66c | |||
| a9bd34f3c5 | |||
| db316b59c5 | |||
| 6209714f87 | |||
| 1ed2bddba7 | |||
| 26b35c9b7b | |||
| 86a9271f75 | |||
| 402ed9bd20 | |||
| 68a0684569 | |||
| bd2e453218 | |||
| 1f31c63e57 | |||
| 480410efa2 | |||
| e9bfee52ed | |||
| 326b574d54 | |||
| 0a7abcf2ad | |||
| 9e5019881e | |||
| 8071750681 | |||
| f2f0931904 | |||
| a91204e5b9 | |||
| b14c22cbff | |||
| b3e40c6aed | |||
| 873aa4bb22 | |||
| c1ea78c422 | |||
| 3c8bbc2621 | |||
| 42a9979d91 | |||
| b7f94df4d9 | |||
| 4143d3fe28 | |||
| f95c06b76f | |||
| e3e9178ccc | |||
| b694816e7b | |||
| e046000f36 | |||
| edb5caae9b | |||
| 02d27651f3 | |||
| 44cd4d847d | |||
| 472256794d | |||
| cbb6887983 | |||
| 317e9ec605 | |||
| ada2a16412 | |||
| 61f6b0f122 | |||
| 6a3f7e45cf | |||
| 2b78c4ba86 | |||
| 680ef641fb | |||
| 2b5504ff63 | |||
| f8a6aa3250 | |||
| 6c23fc4b2b | |||
| 639c2f5c2e | |||
| e44632f9a0 | |||
| 3f2ce34468 | |||
| 426cef998f | |||
| 8ddb62ed0f | |||
| 572f6d4ea0 | |||
| 8db68410c6 | |||
| caa3c3de32 | |||
| 23b5ca761a | |||
| f1b9021e3e | |||
| 99c62af89e | |||
| 8ae50814fe | |||
| 2e2b491ec7 | |||
| ac432e78e2 | |||
| 83ac42ac43 | |||
| 4bd1cd127b | |||
| 2eb5a5cc76 | |||
| 75051687e6 | |||
| 7e316b5fc2 | |||
| 5594ad0b36 | |||
| ea097afeae | |||
| b77b4b5c80 | |||
| f8dc7f48f2 | |||
| 692e75b057 | |||
| 02771683a6 | |||
| 40404ff41d | |||
| fdd5211253 | |||
| 85a417d22e | |||
| 66c530ea06 | |||
| 347c3793fc | |||
| cf78c89830 | |||
| 20c738c384 | |||
| 4f54ce6afb | |||
| f0d7edb963 | |||
| e42ad8db93 | |||
| e917e756cc | |||
| b4963bec76 | |||
| 0d23796989 | |||
| d0ceafe79e | |||
| f2023a7af2 | |||
| 31d597005f | |||
| 62dc86be7b | |||
| 7aa8e35f87 | |||
| 60b95271eb | |||
| 382b0e8941 | |||
| 3b068610b9 | |||
| 9a8f62f42e | |||
| 632e3cf7dc | |||
| e7144649d5 | |||
| dd8909c9b2 | |||
| e6818c1f6a | |||
| 10c4e3c717 | |||
| b8425867c8 | |||
| a05da8cdbf | |||
| c3aeefa653 | |||
| 62c840df21 | |||
| 45d1db8880 | |||
| b34f30f1dd | |||
| 7a54e84eb4 | |||
| 917eef96fb | |||
| 9a393848b2 | |||
| a6abeb50c6 | |||
| 39acb044fb | |||
| 7d2f622f4b | |||
| a2b38caf64 | |||
| 1193b9fd22 | |||
| e3a5ef1907 | |||
| e597bb4542 | |||
| c31df2b3f9 | |||
| 3f2637cffa | |||
| 3b6d9bec0a | |||
| b184210610 | |||
| d2010808ee | |||
| f5b185dd06 | |||
| ae161c1ba9 | |||
| 109283b189 | |||
| 235d283def | |||
| 96a86b3298 | |||
| db9ea8603c | |||
| 8b7f698c7b | |||
| 813c13ce45 | |||
| 629a0e1a4d | |||
| d1e2c018a3 | |||
| 1e86844823 | |||
| b58875d4c7 | |||
| 03e0eecb1d | |||
| 7aa61d86e4 | |||
| 0e6a799e6d | |||
| bc6afdf94f | |||
| 80364b04a9 | |||
| 0948e0ee1c | |||
| 5c54de66fc | |||
| 937edc73bc | |||
| 2c0d8d8943 | |||
| 059ccdd592 | |||
| 0ec0d3f1aa | |||
| 0a0eee138a | |||
| 3ed4c38101 | |||
| de8cf65503 | |||
| 121b36f35f | |||
| 363aed2a47 | |||
| ef994e0084 | |||
| e1ef196283 | |||
| f81ffd54f3 | |||
| f9bfae9190 | |||
| 0d686465a4 | |||
| e13b4a561f | |||
| f6417f95e5 | |||
| 9c6bf5f4ae | |||
| d2d7acb50e | |||
| c7681dde32 | |||
| 8cf9661e08 | |||
| 2dbd76cf90 | |||
| 28d39f4d80 | |||
| 760428aa18 | |||
| 49bbac7441 | |||
| 0b8cfd437b | |||
| b69aaf9417 | |||
| 758d1bdfd4 | |||
| ab501ca971 | |||
| 9657741a3d | |||
| 29b7368f42 | |||
| 75724b6f8d | |||
| 7c9f821bfd | |||
| 5b9e6bed6c | |||
| 6113d7d768 | |||
| 0e3602d7eb | |||
| 2b94e9a687 | |||
| 6ed7d842e4 | |||
| 8794c840cf | |||
| 9c9c00755a | |||
| 6703c0a5d1 | |||
| 060f19ce06 | |||
| b2d2e7cbc8 | |||
| 91fd792f88 | |||
| 2d9cd28221 | |||
| aa64cf898f | |||
| 27d109c1fe | |||
| 1b4a14f3ee | |||
| 9835785864 | |||
| d785998c5a | |||
| 8ba9553220 | |||
| 6eb132c48b | |||
| b523cd064b | |||
| 355b832cc3 | |||
| 8f5af464a2 | |||
| fb70769358 | |||
| ad06778c34 | |||
| bcb4451fb7 | |||
| 110d558572 | |||
| e32d4f0095 | |||
| 0e413acd61 | |||
| d3397c349f | |||
| fb18a10e61 | |||
| 9bb0d04aeb | |||
| 666cf77b04 | |||
| 90ca1b8e5a | |||
| f1e95b8816 | |||
| dad8547212 | |||
| a957e1fc45 | |||
| 39e3f02503 | |||
| 2b999e922c | |||
| 4224134a19 | |||
| eda260dddd | |||
| 8a1dd521e1 | |||
| 1c5e91de1d | |||
| 4b1744fad0 | |||
| f17b83010d | |||
| 12ddf9e73c | |||
| 0b3b300333 | |||
| 23f1a19765 | |||
| b27e998615 | |||
| 2b928146a8 | |||
| a94b0504b7 | |||
| 4fcbfa7709 | |||
| 986e01db20 | |||
| 9092d1189b | |||
| 605ed94ba2 | |||
| 4cbeeb9a0c | |||
| 993dee6aad | |||
| c663deb659 | |||
| 61621e7d60 | |||
| 0ee9b07172 | |||
| 431ba6b4ef | |||
| 146818793e | |||
| 0ce663bce4 | |||
| 923ba4fb42 | |||
| bb6eed0db1 | |||
| d1bd8f333b | |||
| 2ac9f5426d | |||
| 8d1fd48003 | |||
| 241cb01ec6 | |||
| 65b4139997 | |||
| 1431be8c44 | |||
| 049fceeeee | |||
| e6638afa3c | |||
| 465898c7d0 | |||
| c363b1cfde | |||
| b30ffd1318 | |||
| fe0d3a64c8 | |||
| ae9f1c1063 | |||
| ea63d384fd | |||
| c28d75754d | |||
| 518b691e00 | |||
| cd845be45d | |||
| a813d8e05e | |||
| 75f850f4d2 | |||
| c84265c6f0 | |||
| a477ea29cd | |||
| f6aa85e340 | |||
| 0aeedb3ad8 | |||
| 4b29f238b5 | |||
| 34157db06a | |||
| 84b9e66a97 | |||
| e831e4fb94 | |||
| 956922820b | |||
| b0fac9c9f1 | |||
| f4db09cd59 | |||
| 047030f901 | |||
| 638e8d741f | |||
| 425b87a6d0 | |||
| e7dc763612 | |||
| a80cc94da9 | |||
| 547dd3cb7a | |||
| 95739a934c | |||
| d12e24017e | |||
| e4a0345231 | |||
| 078633c2af | |||
| 4b8b800648 | |||
| 6f9ed001a1 | |||
| e4095dfffe | |||
| d5341c2284 | |||
| 357bd65028 | |||
| 867fb0dac0 | |||
| 2666aa2c73 | |||
| f0e9bafa35 | |||
| 0d739f5c1a | |||
| e08077c73a | |||
| 7cf8a31057 | |||
| c43049a981 | |||
| 1a9ace6f9d | |||
| b8d86bc482 | |||
| f7044e41c6 | |||
| fa59fec17a | |||
| e29afa289e | |||
| 4d4193a586 | |||
| 59343ff441 | |||
| cab564152d | |||
| 97b814ab33 | |||
| 88516ba2ca | |||
| f069cfb643 | |||
| 4ce3c2341c | |||
| 77e42d60cb | |||
| cacb919c6f | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2021.10.4 | current_version = 2021.12.1-rc3 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | ||||||
|  | |||||||
							
								
								
									
										205
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										205
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -18,79 +18,17 @@ env: | |||||||
|   POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77" |   POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77" | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   lint-pylint: |   lint: | ||||||
|     runs-on: ubuntu-latest |     strategy: | ||||||
|     steps: |       fail-fast: false | ||||||
|       - uses: actions/checkout@v2 |       matrix: | ||||||
|       - uses: actions/setup-python@v2 |         job: | ||||||
|         with: |           - pylint | ||||||
|           python-version: '3.9' |           - black | ||||||
|       # - id: cache-pipenv |           - isort | ||||||
|       #   uses: actions/cache@v2.1.6 |           - bandit | ||||||
|       #   with: |           - pyright | ||||||
|       #     path: ~/.local/share/virtualenvs |           - pending-migrations | ||||||
|       #     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 |  | ||||||
|   lint-black: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v2 |  | ||||||
|       - 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 |  | ||||||
|   lint-isort: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v2 |  | ||||||
|       - 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 |  | ||||||
|   lint-bandit: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v2 |  | ||||||
|       - 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 |  | ||||||
|   lint-pyright: |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
| @ -100,12 +38,17 @@ jobs: | |||||||
|       - uses: actions/setup-node@v2 |       - uses: actions/setup-node@v2 | ||||||
|         with: |         with: | ||||||
|           node-version: '16' |           node-version: '16' | ||||||
|  |       - id: cache-pipenv | ||||||
|  |         uses: actions/cache@v2.1.7 | ||||||
|  |         with: | ||||||
|  |           path: ~/.local/share/virtualenvs | ||||||
|  |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         run: | |         env: | ||||||
|           scripts/ci_prepare.sh |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|           npm install -g pyright@1.1.136 |         run: scripts/ci_prepare.sh | ||||||
|       - name: run bandit |       - name: run pylint | ||||||
|         run: pipenv run pyright e2e lifecycle |         run: pipenv run make ci-${{ matrix.job }} | ||||||
|   test-migrations: |   test-migrations: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
| @ -113,14 +56,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.7 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: run migrations |       - name: run migrations | ||||||
|         run: pipenv run python -m lifecycle.migrate |         run: pipenv run python -m lifecycle.migrate | ||||||
| @ -137,21 +80,23 @@ jobs: | |||||||
|         id: ev |         id: ev | ||||||
|         run: | |         run: | | ||||||
|           python ./scripts/gh_env.py |           python ./scripts/gh_env.py | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.7 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: checkout stable |       - name: checkout stable | ||||||
|         run: | |         run: | | ||||||
|           # Copy current, latest config to local |           # Copy current, latest config to local | ||||||
|           cp authentik/lib/default.yml local.env.yml |           cp authentik/lib/default.yml local.env.yml | ||||||
|  |           cp -R .github .. | ||||||
|  |           cp -R scripts .. | ||||||
|           git checkout $(git describe --abbrev=0 --match 'version/*') |           git checkout $(git describe --abbrev=0 --match 'version/*') | ||||||
|           git checkout ${{ steps.ev.outputs.branchName }} -- .github |           rm -rf .github/ scripts/ | ||||||
|           git checkout ${{ steps.ev.outputs.branchName }} -- scripts |           mv ../.github ../scripts . | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: | |         run: | | ||||||
|           scripts/ci_prepare.sh |           scripts/ci_prepare.sh | ||||||
|           # Sync anyways since stable will have different dependencies |           # Sync anyways since stable will have different dependencies | ||||||
| @ -162,11 +107,12 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           set -x |           set -x | ||||||
|           git fetch |           git fetch | ||||||
|           git checkout ${{ steps.ev.outputs.branchName }} |           git reset --hard HEAD | ||||||
|  |           git checkout $GITHUB_HEAD_REF | ||||||
|           pipenv sync --dev |           pipenv sync --dev | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - name: migrate to latest |       - name: migrate to latest | ||||||
|         run: pipenv run python -m lifecycle.migrate |         run: pipenv run python -m lifecycle.migrate | ||||||
| @ -177,14 +123,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.7 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - uses: testspace-com/setup-testspace@v1 |       - uses: testspace-com/setup-testspace@v1 | ||||||
|         with: |         with: | ||||||
| @ -206,14 +152,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.7 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: scripts/ci_prepare.sh |         run: scripts/ci_prepare.sh | ||||||
|       - uses: testspace-com/setup-testspace@v1 |       - uses: testspace-com/setup-testspace@v1 | ||||||
|         with: |         with: | ||||||
| @ -245,19 +191,19 @@ jobs: | |||||||
|       - uses: testspace-com/setup-testspace@v1 |       - uses: testspace-com/setup-testspace@v1 | ||||||
|         with: |         with: | ||||||
|           domain: ${{github.repository_owner}} |           domain: ${{github.repository_owner}} | ||||||
|       # - id: cache-pipenv |       - id: cache-pipenv | ||||||
|       #   uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.7 | ||||||
|       #   with: |         with: | ||||||
|       #     path: ~/.local/share/virtualenvs |           path: ~/.local/share/virtualenvs | ||||||
|       #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         # env: |         env: | ||||||
|         #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: | |         run: | | ||||||
|           scripts/ci_prepare.sh |           scripts/ci_prepare.sh | ||||||
|           docker-compose -f tests/e2e/docker-compose.yml up -d |           docker-compose -f tests/e2e/docker-compose.yml up -d | ||||||
|       - id: cache-web |       - id: cache-web | ||||||
|         uses: actions/cache@v2.1.6 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: web/dist |           path: web/dist | ||||||
|           key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }} |           key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }} | ||||||
| @ -277,22 +223,30 @@ jobs: | |||||||
|           testspace [e2e]unittest.xml --link=codecov |           testspace [e2e]unittest.xml --link=codecov | ||||||
|       - if: ${{ always() }} |       - if: ${{ always() }} | ||||||
|         uses: codecov/codecov-action@v2 |         uses: codecov/codecov-action@v2 | ||||||
|   build: |   ci-core-mark: | ||||||
|     needs: |     needs: | ||||||
|       - lint-pylint |       - lint | ||||||
|       - lint-black |  | ||||||
|       - lint-isort |  | ||||||
|       - lint-bandit |  | ||||||
|       - lint-pyright |  | ||||||
|       - test-migrations |       - test-migrations | ||||||
|       - test-migrations-from-stable |       - test-migrations-from-stable | ||||||
|       - test-unittest |       - test-unittest | ||||||
|       - test-integration |       - test-integration | ||||||
|       - test-e2e |       - test-e2e | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - run: echo mark | ||||||
|  |   build: | ||||||
|  |     needs: ci-core-mark | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|     timeout-minutes: 120 |     timeout-minutes: 120 | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         arch: | ||||||
|  |           - 'linux/amd64' | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1.2.0 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v1 | ||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
| @ -317,3 +271,4 @@ jobs: | |||||||
|             ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }} |             ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }} | ||||||
|           build-args: | |           build-args: | | ||||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} |             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||||
|  |           platforms: ${{ matrix.arch }} | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -30,18 +30,29 @@ jobs: | |||||||
|             -w /app \ |             -w /app \ | ||||||
|             golangci/golangci-lint:v1.39.0 \ |             golangci/golangci-lint:v1.39.0 \ | ||||||
|             golangci-lint run -v --timeout 200s |             golangci-lint run -v --timeout 200s | ||||||
|  |   ci-outpost-mark: | ||||||
|  |     needs: | ||||||
|  |       - lint-golint | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - run: echo mark | ||||||
|   build: |   build: | ||||||
|     timeout-minutes: 120 |     timeout-minutes: 120 | ||||||
|     needs: |     needs: | ||||||
|       - lint-golint |       - ci-outpost-mark | ||||||
|     strategy: |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         type: |         type: | ||||||
|           - proxy |           - proxy | ||||||
|           - ldap |           - ldap | ||||||
|  |         arch: | ||||||
|  |           - 'linux/amd64' | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1.2.0 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v1 | ||||||
|       - name: prepare variables |       - name: prepare variables | ||||||
| @ -68,3 +79,4 @@ jobs: | |||||||
|           file: ${{ matrix.type }}.Dockerfile |           file: ${{ matrix.type }}.Dockerfile | ||||||
|           build-args: | |           build-args: | | ||||||
|             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} |             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||||
|  |           platforms: ${{ matrix.arch }} | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							| @ -65,12 +65,18 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           cd web |           cd web | ||||||
|           npm run lit-analyse |           npm run lit-analyse | ||||||
|   build: |   ci-web-mark: | ||||||
|     needs: |     needs: | ||||||
|       - lint-eslint |       - lint-eslint | ||||||
|       - lint-prettier |       - lint-prettier | ||||||
|       - lint-lit-analyse |       - lint-lit-analyse | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - run: echo mark | ||||||
|  |   build: | ||||||
|  |     needs: | ||||||
|  |       - ci-web-mark | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-node@v2 |       - uses: actions/setup-node@v2 | ||||||
|  | |||||||
							
								
								
									
										85
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										85
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -30,14 +30,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik:2021.10.4, |             beryju/authentik:2021.12.1-rc3, | ||||||
|             beryju/authentik:latest, |             beryju/authentik:latest, | ||||||
|             ghcr.io/goauthentik/server:2021.10.4, |             ghcr.io/goauthentik/server:2021.12.1-rc3, | ||||||
|             ghcr.io/goauthentik/server:latest |             ghcr.io/goauthentik/server:latest | ||||||
|           platforms: linux/amd64,linux/arm64 |           platforms: linux/amd64,linux/arm64 | ||||||
|           context: . |           context: . | ||||||
|       - name: Building Docker Image (stable) |       - name: Building Docker Image (stable) | ||||||
|         if: ${{ github.event_name == 'release' && !contains('2021.10.4', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc3', 'rc') }} | ||||||
|         run: | |         run: | | ||||||
|           docker pull beryju/authentik:latest |           docker pull beryju/authentik:latest | ||||||
|           docker tag beryju/authentik:latest beryju/authentik:stable |           docker tag beryju/authentik:latest beryju/authentik:stable | ||||||
| @ -45,8 +45,14 @@ jobs: | |||||||
|           docker pull ghcr.io/goauthentik/server:latest |           docker pull ghcr.io/goauthentik/server:latest | ||||||
|           docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable |           docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable | ||||||
|           docker push ghcr.io/goauthentik/server:stable |           docker push ghcr.io/goauthentik/server:stable | ||||||
|   build-proxy: |   build-outpost: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         type: | ||||||
|  |           - proxy | ||||||
|  |           - ldap | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-go@v2 |       - uses: actions/setup-go@v2 | ||||||
| @ -72,68 +78,25 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik-proxy:2021.10.4, |             beryju/authentik-${{ matrix.type }}:2021.12.1-rc3, | ||||||
|             beryju/authentik-proxy:latest, |             beryju/authentik-${{ matrix.type }}:latest, | ||||||
|             ghcr.io/goauthentik/proxy:2021.10.4, |             ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc3, | ||||||
|             ghcr.io/goauthentik/proxy:latest |             ghcr.io/goauthentik/${{ matrix.type }}:latest | ||||||
|           file: proxy.Dockerfile |           file: ${{ matrix.type }}.Dockerfile | ||||||
|           platforms: linux/amd64,linux/arm64 |           platforms: linux/amd64,linux/arm64 | ||||||
|       - name: Building Docker Image (stable) |       - name: Building Docker Image (stable) | ||||||
|         if: ${{ github.event_name == 'release' && !contains('2021.10.4', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc3', 'rc') }} | ||||||
|         run: | |         run: | | ||||||
|           docker pull beryju/authentik-proxy:latest |           docker pull beryju/authentik-${{ matrix.type }}:latest | ||||||
|           docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable |           docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable | ||||||
|           docker push beryju/authentik-proxy:stable |           docker push beryju/authentik-${{ matrix.type }}:stable | ||||||
|           docker pull ghcr.io/goauthentik/proxy:latest |           docker pull ghcr.io/goauthentik/${{ matrix.type }}:latest | ||||||
|           docker tag ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:stable |           docker tag ghcr.io/goauthentik/${{ matrix.type }}:latest ghcr.io/goauthentik/${{ matrix.type }}:stable | ||||||
|           docker push ghcr.io/goauthentik/proxy:stable |           docker push ghcr.io/goauthentik/${{ matrix.type }}:stable | ||||||
|   build-ldap: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v2 |  | ||||||
|       - uses: actions/setup-go@v2 |  | ||||||
|         with: |  | ||||||
|           go-version: "^1.15" |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v1.2.0 |  | ||||||
|       - name: Set up Docker Buildx |  | ||||||
|         uses: docker/setup-buildx-action@v1 |  | ||||||
|       - name: Docker Login Registry |  | ||||||
|         uses: docker/login-action@v1 |  | ||||||
|         with: |  | ||||||
|           username: ${{ secrets.DOCKER_USERNAME }} |  | ||||||
|           password: ${{ secrets.DOCKER_PASSWORD }} |  | ||||||
|       - name: Login to GitHub Container Registry |  | ||||||
|         uses: docker/login-action@v1 |  | ||||||
|         with: |  | ||||||
|           registry: ghcr.io |  | ||||||
|           username: ${{ github.repository_owner }} |  | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|       - name: Building Docker Image |  | ||||||
|         uses: docker/build-push-action@v2 |  | ||||||
|         with: |  | ||||||
|           push: ${{ github.event_name == 'release' }} |  | ||||||
|           tags: | |  | ||||||
|             beryju/authentik-ldap:2021.10.4, |  | ||||||
|             beryju/authentik-ldap:latest, |  | ||||||
|             ghcr.io/goauthentik/ldap:2021.10.4, |  | ||||||
|             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.10.4', 'rc') }} |  | ||||||
|         run: | |  | ||||||
|           docker pull beryju/authentik-ldap:latest |  | ||||||
|           docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable |  | ||||||
|           docker push beryju/authentik-ldap:stable |  | ||||||
|           docker pull ghcr.io/goauthentik/ldap:latest |  | ||||||
|           docker tag ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:stable |  | ||||||
|           docker push ghcr.io/goauthentik/ldap:stable |  | ||||||
|   test-release: |   test-release: | ||||||
|     needs: |     needs: | ||||||
|       - build-server |       - build-server | ||||||
|       - build-proxy |       - build-outpost | ||||||
|       - build-ldap |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
| @ -170,7 +133,7 @@ jobs: | |||||||
|           SENTRY_PROJECT: authentik |           SENTRY_PROJECT: authentik | ||||||
|           SENTRY_URL: https://sentry.beryju.org |           SENTRY_URL: https://sentry.beryju.org | ||||||
|         with: |         with: | ||||||
|           version: authentik@2021.10.4 |           version: authentik@2021.12.1-rc3 | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|           sourcemaps: './web/dist' |           sourcemaps: './web/dist' | ||||||
|           url_prefix: '~/static/dist' |           url_prefix: '~/static/dist' | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -15,6 +15,7 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           echo "PG_PASS=$(openssl rand -base64 32)" >> .env |           echo "PG_PASS=$(openssl rand -base64 32)" >> .env | ||||||
|           echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env |           echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env | ||||||
|  |           docker buildx install | ||||||
|           docker build \ |           docker build \ | ||||||
|             --no-cache \ |             --no-cache \ | ||||||
|             -t testing:latest \ |             -t testing:latest \ | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							| @ -4,6 +4,9 @@ on: | |||||||
|     branches: [ master ] |     branches: [ master ] | ||||||
|     paths: |     paths: | ||||||
|       - '/locale/' |       - '/locale/' | ||||||
|  |   pull_request: | ||||||
|  |     paths: | ||||||
|  |       - '/locale/' | ||||||
|   schedule: |   schedule: | ||||||
|   - cron: "0 */2 * * *" |   - cron: "0 */2 * * *" | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
| @ -21,7 +24,14 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.9' |           python-version: '3.9' | ||||||
|  |       - id: cache-pipenv | ||||||
|  |         uses: actions/cache@v2.1.7 | ||||||
|  |         with: | ||||||
|  |           path: ~/.local/share/virtualenvs | ||||||
|  |           key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|         run: | |         run: | | ||||||
|           sudo apt-get update |           sudo apt-get update | ||||||
|           sudo apt-get install -y gettext |           sudo apt-get install -y gettext | ||||||
| @ -30,10 +40,19 @@ jobs: | |||||||
|         run: pipenv run ./manage.py compilemessages |         run: pipenv run ./manage.py compilemessages | ||||||
|       - name: Create Pull Request |       - name: Create Pull Request | ||||||
|         uses: peter-evans/create-pull-request@v3 |         uses: peter-evans/create-pull-request@v3 | ||||||
|  |         id: cpr | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.GITHUB_TOKEN }} |           token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           branch: compile-backend-translation |           branch: compile-backend-translation | ||||||
|           commit-message: "core: compile backend translations" |           commit-message: "core: compile backend translations" | ||||||
|           title: "core: compile backend translations" |           title: "core: compile backend translations" | ||||||
|  |           body: "core: compile backend translations" | ||||||
|           delete-branch: true |           delete-branch: true | ||||||
|           signoff: true |           signoff: true | ||||||
|  |       - name: Enable Pull Request Automerge | ||||||
|  |         if: steps.cpr.outputs.pull-request-operation == 'created' | ||||||
|  |         uses: peter-evans/enable-pull-request-automerge@v1 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} | ||||||
|  |           merge-method: squash | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -30,10 +30,19 @@ jobs: | |||||||
|           npm i @goauthentik/api@$VERSION |           npm i @goauthentik/api@$VERSION | ||||||
|       - name: Create Pull Request |       - name: Create Pull Request | ||||||
|         uses: peter-evans/create-pull-request@v3 |         uses: peter-evans/create-pull-request@v3 | ||||||
|  |         id: cpr | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.GITHUB_TOKEN }} |           token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           branch: update-web-api-client |           branch: update-web-api-client | ||||||
|           commit-message: "web: Update Web API Client version" |           commit-message: "web: Update Web API Client version" | ||||||
|           title: "web: Update Web API Client version" |           title: "web: Update Web API Client version" | ||||||
|  |           body: "web: Update Web API Client version" | ||||||
|           delete-branch: true |           delete-branch: true | ||||||
|           signoff: true |           signoff: true | ||||||
|  |       - name: Enable Pull Request Automerge | ||||||
|  |         if: steps.cpr.outputs.pull-request-operation == 'created' | ||||||
|  |         uses: peter-evans/enable-pull-request-automerge@v1 | ||||||
|  |         with: | ||||||
|  |           token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} | ||||||
|  |           merge-method: squash | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -66,7 +66,9 @@ coverage.xml | |||||||
| unittest.xml | unittest.xml | ||||||
|  |  | ||||||
| # Translations | # Translations | ||||||
| *.mo | # Have to include binary mo files as they are annoying to compile at build time | ||||||
|  | # since a full postgres and redis instance are required | ||||||
|  | # *.mo | ||||||
|  |  | ||||||
| # Django stuff: | # Django stuff: | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -10,7 +10,8 @@ | |||||||
|         "plex", |         "plex", | ||||||
|         "saml", |         "saml", | ||||||
|         "totp", |         "totp", | ||||||
|         "webauthn" |         "webauthn", | ||||||
|  |         "traefik" | ||||||
|     ], |     ], | ||||||
|     "python.linting.pylintEnabled": true, |     "python.linting.pylintEnabled": true, | ||||||
|     "todo-tree.tree.showCountsInTree": true, |     "todo-tree.tree.showCountsInTree": true, | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | |||||||
| # Stage 1: Lock python dependencies | # Stage 1: Lock python dependencies | ||||||
| FROM docker.io/python:3.9-slim-bullseye as locker | FROM docker.io/python:3.10.1-slim-bullseye as locker | ||||||
|  |  | ||||||
| COPY ./Pipfile /app/ | COPY ./Pipfile /app/ | ||||||
| COPY ./Pipfile.lock /app/ | COPY ./Pipfile.lock /app/ | ||||||
| @ -11,35 +11,32 @@ RUN pip install pipenv && \ | |||||||
|     pipenv lock -r --dev-only > requirements-dev.txt |     pipenv lock -r --dev-only > requirements-dev.txt | ||||||
|  |  | ||||||
| # Stage 2: Build website | # Stage 2: Build website | ||||||
| FROM docker.io/node:16 as website-builder | FROM --platform=${BUILDPLATFORM} docker.io/node:16 as website-builder | ||||||
|  |  | ||||||
| COPY ./website /static/ | COPY ./website /work/website/ | ||||||
|  |  | ||||||
| ENV NODE_ENV=production | ENV NODE_ENV=production | ||||||
| RUN cd /static && npm i && npm run build-docs-only | RUN cd /work/website && npm i && npm run build-docs-only | ||||||
|  |  | ||||||
| # Stage 3: Build webui | # Stage 3: Build webui | ||||||
| FROM docker.io/node:16 as web-builder | FROM --platform=${BUILDPLATFORM} docker.io/node:16 as web-builder | ||||||
|  |  | ||||||
| COPY ./web /static/ | COPY ./web /work/web/ | ||||||
|  | COPY ./website /work/website/ | ||||||
|  |  | ||||||
| ENV NODE_ENV=production | ENV NODE_ENV=production | ||||||
| RUN cd /static && npm i && npm run build | RUN cd /work/web && npm i && npm run build | ||||||
|  |  | ||||||
| # Stage 4: Build go proxy | # Stage 4: Build go proxy | ||||||
| FROM docker.io/golang:1.17.3-bullseye AS builder | FROM docker.io/golang:1.17.4-bullseye AS builder | ||||||
|  |  | ||||||
| WORKDIR /work | WORKDIR /work | ||||||
|  |  | ||||||
| COPY --from=web-builder /static/robots.txt /work/web/robots.txt | COPY --from=web-builder /work/web/robots.txt /work/web/robots.txt | ||||||
| COPY --from=web-builder /static/security.txt /work/web/security.txt | COPY --from=web-builder /work/web/security.txt /work/web/security.txt | ||||||
| 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 ./cmd /work/cmd | COPY ./cmd /work/cmd | ||||||
| COPY ./web/static.go /work/web/static.go | COPY ./web/static.go /work/web/static.go | ||||||
| COPY ./website/static.go /work/website/static.go |  | ||||||
| COPY ./internal /work/internal | COPY ./internal /work/internal | ||||||
| COPY ./go.mod /work/go.mod | COPY ./go.mod /work/go.mod | ||||||
| COPY ./go.sum /work/go.sum | COPY ./go.sum /work/go.sum | ||||||
| @ -47,7 +44,7 @@ COPY ./go.sum /work/go.sum | |||||||
| RUN go build -o /work/authentik ./cmd/server/main.go | RUN go build -o /work/authentik ./cmd/server/main.go | ||||||
|  |  | ||||||
| # Stage 5: Run | # Stage 5: Run | ||||||
| FROM docker.io/python:3.9-slim-bullseye | FROM docker.io/python:3.10.1-slim-bullseye | ||||||
|  |  | ||||||
| WORKDIR / | WORKDIR / | ||||||
| COPY --from=locker /app/requirements.txt / | COPY --from=locker /app/requirements.txt / | ||||||
| @ -57,19 +54,18 @@ ARG GIT_BUILD_HASH | |||||||
| ENV GIT_BUILD_HASH=$GIT_BUILD_HASH | ENV GIT_BUILD_HASH=$GIT_BUILD_HASH | ||||||
|  |  | ||||||
| RUN apt-get update && \ | RUN apt-get update && \ | ||||||
|     apt-get install -y --no-install-recommends curl ca-certificates gnupg git runit && \ |     apt-get install -y --no-install-recommends \ | ||||||
|     curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ |         curl ca-certificates gnupg git runit libpq-dev \ | ||||||
|     echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ |         postgresql-client build-essential libxmlsec1-dev \ | ||||||
|     apt-get update && \ |         pkg-config libmaxminddb0 && \ | ||||||
|     apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \ |  | ||||||
|     pip install -r /requirements.txt --no-cache-dir && \ |     pip install -r /requirements.txt --no-cache-dir && \ | ||||||
|     apt-get remove --purge -y build-essential git && \ |     apt-get remove --purge -y build-essential git && \ | ||||||
|     apt-get autoremove --purge -y && \ |     apt-get autoremove --purge -y && \ | ||||||
|     apt-get clean && \ |     apt-get clean && \ | ||||||
|     rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \ |     rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \ | ||||||
|     adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \ |     adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \ | ||||||
|     mkdir /backups && \ |     mkdir /backups /certs && \ | ||||||
|     chown authentik:authentik /backups |     chown authentik:authentik /backups /certs | ||||||
|  |  | ||||||
| COPY ./authentik/ /authentik | COPY ./authentik/ /authentik | ||||||
| COPY ./pyproject.toml / | COPY ./pyproject.toml / | ||||||
| @ -78,6 +74,9 @@ COPY ./tests /tests | |||||||
| COPY ./manage.py / | COPY ./manage.py / | ||||||
| COPY ./lifecycle/ /lifecycle | COPY ./lifecycle/ /lifecycle | ||||||
| COPY --from=builder /work/authentik /authentik-proxy | COPY --from=builder /work/authentik /authentik-proxy | ||||||
|  | COPY --from=web-builder /work/web/dist/ /web/dist/ | ||||||
|  | COPY --from=web-builder /work/web/authentik/ /web/authentik/ | ||||||
|  | COPY --from=website-builder /work/website/help/ /website/help/ | ||||||
|  |  | ||||||
| USER authentik | USER authentik | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								Makefile
									
									
									
									
									
								
							| @ -7,13 +7,13 @@ NPM_VERSION = $(shell python -m scripts.npm_version) | |||||||
| all: lint-fix lint test gen | all: lint-fix lint test gen | ||||||
|  |  | ||||||
| test-integration: | test-integration: | ||||||
| 	coverage run manage.py test -v 3 tests/integration | 	coverage run manage.py test tests/integration | ||||||
|  |  | ||||||
| test-e2e: | test-e2e: | ||||||
| 	coverage run manage.py test --failfast -v 3 tests/e2e | 	coverage run manage.py test tests/e2e | ||||||
|  |  | ||||||
| test: | test: | ||||||
| 	coverage run manage.py test -v 3 authentik | 	coverage run manage.py test authentik | ||||||
| 	coverage html | 	coverage html | ||||||
| 	coverage report | 	coverage report | ||||||
|  |  | ||||||
| @ -33,9 +33,10 @@ lint: | |||||||
| 	bandit -r authentik tests lifecycle -x node_modules | 	bandit -r authentik tests lifecycle -x node_modules | ||||||
| 	pylint authentik tests lifecycle | 	pylint authentik tests lifecycle | ||||||
|  |  | ||||||
| i18n-extract: | i18n-extract: i18n-extract-core web-extract | ||||||
|  |  | ||||||
|  | i18n-extract-core: | ||||||
| 	./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en | 	./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en | ||||||
| 	cd web && npm run extract |  | ||||||
|  |  | ||||||
| gen-build: | gen-build: | ||||||
| 	./manage.py spectacular --file schema.yml | 	./manage.py spectacular --file schema.yml | ||||||
| @ -48,7 +49,7 @@ gen-web: | |||||||
| 	docker run \ | 	docker run \ | ||||||
| 		--rm -v ${PWD}:/local \ | 		--rm -v ${PWD}:/local \ | ||||||
| 		--user ${UID}:${GID} \ | 		--user ${UID}:${GID} \ | ||||||
| 		ghcr.io/beryju/openapi-generator generate \ | 		openapitools/openapi-generator-cli generate \ | ||||||
| 		-i /local/schema.yml \ | 		-i /local/schema.yml \ | ||||||
| 		-g typescript-fetch \ | 		-g typescript-fetch \ | ||||||
| 		-o /local/web-api \ | 		-o /local/web-api \ | ||||||
| @ -67,12 +68,13 @@ gen-outpost: | |||||||
| 	docker run \ | 	docker run \ | ||||||
| 		--rm -v ${PWD}:/local \ | 		--rm -v ${PWD}:/local \ | ||||||
| 		--user ${UID}:${GID} \ | 		--user ${UID}:${GID} \ | ||||||
| 		openapitools/openapi-generator-cli generate \ | 		openapitools/openapi-generator-cli:v5.2.1 generate \ | ||||||
| 		-i /local/schema.yml \ | 		-i /local/schema.yml \ | ||||||
| 		-g go \ | 		-g go \ | ||||||
| 		-o /local/api \ | 		-o /local/api \ | ||||||
| 		-c /local/config.yaml | 		-c /local/config.yaml | ||||||
| 	go mod edit -replace goauthentik.io/api=./api | 	go mod edit -replace goauthentik.io/api=./api | ||||||
|  | 	rm -rf config.yaml ./templates/ | ||||||
|  |  | ||||||
| gen: gen-build gen-clean gen-web | gen: gen-build gen-clean gen-web | ||||||
|  |  | ||||||
| @ -81,3 +83,39 @@ migrate: | |||||||
|  |  | ||||||
| run: | run: | ||||||
| 	go run -v cmd/server/main.go | 	go run -v cmd/server/main.go | ||||||
|  |  | ||||||
|  | web-watch: | ||||||
|  | 	cd web && npm run watch | ||||||
|  |  | ||||||
|  | web: web-lint-fix web-lint web-extract | ||||||
|  |  | ||||||
|  | web-lint-fix: | ||||||
|  | 	cd web && npm run prettier | ||||||
|  |  | ||||||
|  | web-lint: | ||||||
|  | 	cd web && npm run lint | ||||||
|  | 	cd web && npm run lit-analyse | ||||||
|  |  | ||||||
|  | web-extract: | ||||||
|  | 	cd web && npm run extract | ||||||
|  |  | ||||||
|  | # These targets are use by GitHub actions to allow usage of matrix | ||||||
|  | # which makes the YAML File a lot smaller | ||||||
|  |  | ||||||
|  | ci-pylint: | ||||||
|  | 	pylint authentik tests lifecycle | ||||||
|  |  | ||||||
|  | ci-black: | ||||||
|  | 	black --check authentik tests lifecycle | ||||||
|  |  | ||||||
|  | ci-isort: | ||||||
|  | 	isort --check authentik tests lifecycle | ||||||
|  |  | ||||||
|  | ci-bandit: | ||||||
|  | 	bandit -r authentik tests lifecycle | ||||||
|  |  | ||||||
|  | ci-pyright: | ||||||
|  | 	pyright e2e lifecycle | ||||||
|  |  | ||||||
|  | ci-pending-migrations: | ||||||
|  | 	./manage.py makemigrations --check | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								Pipfile
									
									
									
									
									
								
							| @ -8,7 +8,10 @@ boto3 = "*" | |||||||
| celery = "*" | celery = "*" | ||||||
| channels = "*" | channels = "*" | ||||||
| channels-redis = "*" | channels-redis = "*" | ||||||
|  | codespell = "*" | ||||||
|  | colorama = "*" | ||||||
| dacite = "*" | dacite = "*" | ||||||
|  | deepmerge = "*" | ||||||
| defusedxml = "*" | defusedxml = "*" | ||||||
| django = "*" | django = "*" | ||||||
| django-dbbackup = { git = 'https://github.com/django-dbbackup/django-dbbackup.git', ref = '9d1909c30a3271c8c9c8450add30d6e0b996e145' } | django-dbbackup = { git = 'https://github.com/django-dbbackup/django-dbbackup.git', ref = '9d1909c30a3271c8c9c8450add30d6e0b996e145' } | ||||||
| @ -23,6 +26,7 @@ djangorestframework = "*" | |||||||
| djangorestframework-guardian = "*" | djangorestframework-guardian = "*" | ||||||
| docker = "*" | docker = "*" | ||||||
| drf-spectacular = "*" | drf-spectacular = "*" | ||||||
|  | duo-client = "*" | ||||||
| facebook-sdk = "*" | facebook-sdk = "*" | ||||||
| geoip2 = "*" | geoip2 = "*" | ||||||
| gunicorn = "*" | gunicorn = "*" | ||||||
| @ -40,19 +44,16 @@ service_identity = "*" | |||||||
| structlog = "*" | structlog = "*" | ||||||
| swagger-spec-validator = "*" | swagger-spec-validator = "*" | ||||||
| twisted = "==21.7.0" | twisted = "==21.7.0" | ||||||
|  | ua-parser = "*" | ||||||
| urllib3 = {extras = ["secure"],version = "*"} | urllib3 = {extras = ["secure"],version = "*"} | ||||||
| uvicorn = {extras = ["standard"],version = "*"} | uvicorn = {extras = ["standard"],version = "*"} | ||||||
| webauthn = "*" | webauthn = "*" | ||||||
| xmlsec = "*" | xmlsec = "*" | ||||||
| duo-client = "*" | flower = "*" | ||||||
| ua-parser = "*" |  | ||||||
| deepmerge = "*" |  | ||||||
| colorama = "*" |  | ||||||
| codespell = "*" |  | ||||||
|  |  | ||||||
| [dev-packages] | [dev-packages] | ||||||
| bandit = "*" | bandit = "*" | ||||||
| black = "==21.9b0" | black = "==21.11b1" | ||||||
| bump2version = "*" | bump2version = "*" | ||||||
| colorama = "*" | colorama = "*" | ||||||
| coverage = {extras = ["toml"],version = "*"} | coverage = {extras = ["toml"],version = "*"} | ||||||
| @ -60,5 +61,7 @@ pylint = "*" | |||||||
| pylint-django = "*" | pylint-django = "*" | ||||||
| pytest = "*" | pytest = "*" | ||||||
| pytest-django = "*" | pytest-django = "*" | ||||||
| selenium = "*" | pytest-randomly = "*" | ||||||
| requests-mock = "*" | requests-mock = "*" | ||||||
|  | selenium = "*" | ||||||
|  | importlib-metadata = "*" | ||||||
|  | |||||||
							
								
								
									
										912
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										912
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,3 +1,3 @@ | |||||||
| """authentik""" | """authentik""" | ||||||
| __version__ = "2021.10.4" | __version__ = "2021.12.1-rc3" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  | |||||||
| @ -86,7 +86,7 @@ class SystemSerializer(PassiveSerializer): | |||||||
|     def get_embedded_outpost_host(self, request: Request) -> str: |     def get_embedded_outpost_host(self, request: Request) -> str: | ||||||
|         """Get the FQDN configured on the embedded outpost""" |         """Get the FQDN configured on the embedded outpost""" | ||||||
|         outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) |         outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) | ||||||
|         if not outposts.exists(): |         if not outposts.exists():  # pragma: no cover | ||||||
|             return "" |             return "" | ||||||
|         return outposts.first().config.authentik_host |         return outposts.first().config.authentik_host | ||||||
|  |  | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ class TaskSerializer(PassiveSerializer): | |||||||
|         are pickled in cache. In that case, just delete the info""" |         are pickled in cache. In that case, just delete the info""" | ||||||
|         try: |         try: | ||||||
|             return super().to_representation(instance) |             return super().to_representation(instance) | ||||||
|         except AttributeError: |         except AttributeError:  # pragma: no cover | ||||||
|             if isinstance(self.instance, list): |             if isinstance(self.instance, list): | ||||||
|                 for inst in self.instance: |                 for inst in self.instance: | ||||||
|                     inst.delete() |                     inst.delete() | ||||||
|  | |||||||
| @ -23,6 +23,6 @@ class WorkerView(APIView): | |||||||
|         """Get currently connected worker count.""" |         """Get currently connected worker count.""" | ||||||
|         count = len(CELERY_APP.control.ping(timeout=0.5)) |         count = len(CELERY_APP.control.ping(timeout=0.5)) | ||||||
|         # In debug we run with `CELERY_TASK_ALWAYS_EAGER`, so tasks are ran on the main process |         # In debug we run with `CELERY_TASK_ALWAYS_EAGER`, so tasks are ran on the main process | ||||||
|         if settings.DEBUG: |         if settings.DEBUG:  # pragma: no cover | ||||||
|             count += 1 |             count += 1 | ||||||
|         return Response({"count": count}) |         return Response({"count": count}) | ||||||
|  | |||||||
| @ -54,7 +54,7 @@ def clear_update_notifications(): | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def update_latest_version(self: MonitoredTask): | def update_latest_version(self: MonitoredTask): | ||||||
|     """Update latest version info""" |     """Update latest version info""" | ||||||
|     if CONFIG.y_bool("disable_update_check"): |     if CONFIG.y_bool("disable_update_check"): | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from authentik import __version__ | |||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.core.tasks import clean_expired_models | from authentik.core.tasks import clean_expired_models | ||||||
| from authentik.events.monitored_tasks import TaskResultStatus | from authentik.events.monitored_tasks import TaskResultStatus | ||||||
|  | from authentik.managed.tasks import managed_reconcile | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestAdminAPI(TestCase): | class TestAdminAPI(TestCase): | ||||||
| @ -94,5 +95,7 @@ class TestAdminAPI(TestCase): | |||||||
|  |  | ||||||
|     def test_system(self): |     def test_system(self): | ||||||
|         """Test system API""" |         """Test system API""" | ||||||
|  |         # pyright: reportGeneralTypeIssues=false | ||||||
|  |         managed_reconcile()  # pylint: disable=no-value-for-parameter | ||||||
|         response = self.client.get(reverse("authentik_api:admin_system")) |         response = self.client.get(reverse("authentik_api:admin_system")) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  | |||||||
| @ -3,8 +3,13 @@ from django.core.cache import cache | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from requests_mock import Mocker | from requests_mock import Mocker | ||||||
|  |  | ||||||
| from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version | from authentik.admin.tasks import ( | ||||||
|  |     VERSION_CACHE_KEY, | ||||||
|  |     clear_update_notifications, | ||||||
|  |     update_latest_version, | ||||||
|  | ) | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
|  |  | ||||||
| RESPONSE_VALID = { | RESPONSE_VALID = { | ||||||
|     "$schema": "https://version.goauthentik.io/schema.json", |     "$schema": "https://version.goauthentik.io/schema.json", | ||||||
| @ -56,3 +61,23 @@ class TestAdminTasks(TestCase): | |||||||
|                     action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" |                     action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" | ||||||
|                 ).exists() |                 ).exists() | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     def test_version_disabled(self): | ||||||
|  |         """Test Update checker while its disabled""" | ||||||
|  |         with CONFIG.patch("disable_update_check", True): | ||||||
|  |             update_latest_version.delay().get() | ||||||
|  |             self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0") | ||||||
|  |  | ||||||
|  |     def test_clear_update_notifications(self): | ||||||
|  |         """Test clear of previous notification""" | ||||||
|  |         Event.objects.create( | ||||||
|  |             action=EventAction.UPDATE_AVAILABLE, context={"new_version": "99999999.9999999.9999999"} | ||||||
|  |         ) | ||||||
|  |         Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={"new_version": "1.1.1"}) | ||||||
|  |         Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={}) | ||||||
|  |         clear_update_notifications() | ||||||
|  |         self.assertFalse( | ||||||
|  |             Event.objects.filter( | ||||||
|  |                 action=EventAction.UPDATE_AVAILABLE, context__new_version="1.1" | ||||||
|  |             ).exists() | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -1,18 +0,0 @@ | |||||||
| """Throttling classes""" |  | ||||||
| from typing import Type |  | ||||||
|  |  | ||||||
| from django.views import View |  | ||||||
| from rest_framework.request import Request |  | ||||||
| from rest_framework.throttling import ScopedRateThrottle |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SessionThrottle(ScopedRateThrottle): |  | ||||||
|     """Throttle based on session key""" |  | ||||||
|  |  | ||||||
|     def allow_request(self, request: Request, view): |  | ||||||
|         if request._request.user.is_superuser: |  | ||||||
|             return True |  | ||||||
|         return super().allow_request(request, view) |  | ||||||
|  |  | ||||||
|     def get_cache_key(self, request: Request, view: Type[View]) -> str: |  | ||||||
|         return f"authentik-throttle-session-{request._request.session.session_key}" |  | ||||||
| @ -5,7 +5,14 @@ from django.conf import settings | |||||||
| from django.db import models | from django.db import models | ||||||
| from drf_spectacular.utils import extend_schema | from drf_spectacular.utils import extend_schema | ||||||
| from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME | from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME | ||||||
| from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField, ListField | from rest_framework.fields import ( | ||||||
|  |     BooleanField, | ||||||
|  |     CharField, | ||||||
|  |     ChoiceField, | ||||||
|  |     FloatField, | ||||||
|  |     IntegerField, | ||||||
|  |     ListField, | ||||||
|  | ) | ||||||
| from rest_framework.permissions import AllowAny | from rest_framework.permissions import AllowAny | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| @ -24,13 +31,19 @@ class Capabilities(models.TextChoices): | |||||||
|     CAN_BACKUP = "can_backup" |     CAN_BACKUP = "can_backup" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ErrorReportingConfigSerializer(PassiveSerializer): | ||||||
|  |     """Config for error reporting""" | ||||||
|  |  | ||||||
|  |     enabled = BooleanField(read_only=True) | ||||||
|  |     environment = CharField(read_only=True) | ||||||
|  |     send_pii = BooleanField(read_only=True) | ||||||
|  |     traces_sample_rate = FloatField(read_only=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ConfigSerializer(PassiveSerializer): | class ConfigSerializer(PassiveSerializer): | ||||||
|     """Serialize authentik Config into DRF Object""" |     """Serialize authentik Config into DRF Object""" | ||||||
|  |  | ||||||
|     error_reporting_enabled = BooleanField(read_only=True) |     error_reporting = ErrorReportingConfigSerializer(required=True) | ||||||
|     error_reporting_environment = CharField(read_only=True) |  | ||||||
|     error_reporting_send_pii = BooleanField(read_only=True) |  | ||||||
|  |  | ||||||
|     capabilities = ListField(child=ChoiceField(choices=Capabilities.choices)) |     capabilities = ListField(child=ChoiceField(choices=Capabilities.choices)) | ||||||
|  |  | ||||||
|     cache_timeout = IntegerField(required=True) |     cache_timeout = IntegerField(required=True) | ||||||
| @ -66,9 +79,12 @@ class ConfigView(APIView): | |||||||
|         """Retrieve public configuration options""" |         """Retrieve public configuration options""" | ||||||
|         config = ConfigSerializer( |         config = ConfigSerializer( | ||||||
|             { |             { | ||||||
|                 "error_reporting_enabled": CONFIG.y("error_reporting.enabled"), |                 "error_reporting": { | ||||||
|                 "error_reporting_environment": CONFIG.y("error_reporting.environment"), |                     "enabled": CONFIG.y("error_reporting.enabled"), | ||||||
|                 "error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"), |                     "environment": CONFIG.y("error_reporting.environment"), | ||||||
|  |                     "send_pii": CONFIG.y("error_reporting.send_pii"), | ||||||
|  |                     "traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)), | ||||||
|  |                 }, | ||||||
|                 "capabilities": self.get_capabilities(), |                 "capabilities": self.get_capabilities(), | ||||||
|                 "cache_timeout": int(CONFIG.y("redis.cache_timeout")), |                 "cache_timeout": int(CONFIG.y("redis.cache_timeout")), | ||||||
|                 "cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")), |                 "cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")), | ||||||
|  | |||||||
| @ -56,6 +56,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri | |||||||
|             "component", |             "component", | ||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|  |             "meta_model_name", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -43,6 +43,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "assigned_application_name", |             "assigned_application_name", | ||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|  |             "meta_model_name", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -48,6 +48,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "component", |             "component", | ||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|  |             "meta_model_name", | ||||||
|             "policy_engine_mode", |             "policy_engine_mode", | ||||||
|             "user_matching_mode", |             "user_matching_mode", | ||||||
|         ] |         ] | ||||||
|  | |||||||
| @ -55,6 +55,7 @@ from authentik.core.models import ( | |||||||
|     User, |     User, | ||||||
| ) | ) | ||||||
| from authentik.events.models import EventAction | from authentik.events.models import EventAction | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.stages.email.tasks import send_mails | from authentik.stages.email.tasks import send_mails | ||||||
| from authentik.stages.email.utils import TemplateEmailMessage | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| @ -125,7 +126,9 @@ class UserSelfSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|     def validate_email(self, email: str): |     def validate_email(self, email: str): | ||||||
|         """Check if the user is allowed to change their email""" |         """Check if the user is allowed to change their email""" | ||||||
|         if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_EMAIL, True): |         if self.instance.group_attributes().get( | ||||||
|  |             USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.y_bool("default_user_change_email", True) | ||||||
|  |         ): | ||||||
|             return email |             return email | ||||||
|         if email != self.instance.email: |         if email != self.instance.email: | ||||||
|             raise ValidationError("Not allowed to change email.") |             raise ValidationError("Not allowed to change email.") | ||||||
| @ -133,7 +136,9 @@ class UserSelfSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|     def validate_username(self, username: str): |     def validate_username(self, username: str): | ||||||
|         """Check if the user is allowed to change their username""" |         """Check if the user is allowed to change their username""" | ||||||
|         if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True): |         if self.instance.group_attributes().get( | ||||||
|  |             USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.y_bool("default_user_change_username", True) | ||||||
|  |         ): | ||||||
|             return username |             return username | ||||||
|         if username != self.instance.username: |         if username != self.instance.username: | ||||||
|             raise ValidationError("Not allowed to change username.") |             raise ValidationError("Not allowed to change username.") | ||||||
| @ -309,7 +314,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|                     name=username, |                     name=username, | ||||||
|                     attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False}, |                     attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False}, | ||||||
|                 ) |                 ) | ||||||
|                 if create_group: |                 if create_group and self.request.user.has_perm("authentik_core.add_group"): | ||||||
|                     group = Group.objects.create( |                     group = Group.objects.create( | ||||||
|                         name=username, |                         name=username, | ||||||
|                     ) |                     ) | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ class MetaNameSerializer(PassiveSerializer): | |||||||
|  |  | ||||||
|     verbose_name = SerializerMethodField() |     verbose_name = SerializerMethodField() | ||||||
|     verbose_name_plural = SerializerMethodField() |     verbose_name_plural = SerializerMethodField() | ||||||
|  |     meta_model_name = SerializerMethodField() | ||||||
|  |  | ||||||
|     def get_verbose_name(self, obj: Model) -> str: |     def get_verbose_name(self, obj: Model) -> str: | ||||||
|         """Return object's verbose_name""" |         """Return object's verbose_name""" | ||||||
| @ -50,6 +51,10 @@ class MetaNameSerializer(PassiveSerializer): | |||||||
|         """Return object's plural verbose_name""" |         """Return object's plural verbose_name""" | ||||||
|         return obj._meta.verbose_name_plural |         return obj._meta.verbose_name_plural | ||||||
|  |  | ||||||
|  |     def get_meta_model_name(self, obj: Model) -> str: | ||||||
|  |         """Return internal model name""" | ||||||
|  |         return f"{obj._meta.app_label}.{obj._meta.model_name}" | ||||||
|  |  | ||||||
|  |  | ||||||
| class TypeCreateSerializer(PassiveSerializer): | class TypeCreateSerializer(PassiveSerializer): | ||||||
|     """Types of an object that can be created""" |     """Types of an object that can be created""" | ||||||
|  | |||||||
| @ -1,15 +0,0 @@ | |||||||
| """Output full config""" |  | ||||||
| from json import dumps |  | ||||||
|  |  | ||||||
| from django.core.management.base import BaseCommand, no_translations |  | ||||||
|  |  | ||||||
| from authentik.lib.config import CONFIG |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Command(BaseCommand):  # pragma: no cover |  | ||||||
|     """Output full config""" |  | ||||||
|  |  | ||||||
|     @no_translations |  | ||||||
|     def handle(self, *args, **options): |  | ||||||
|         """Check permissions for all apps""" |  | ||||||
|         print(dumps(CONFIG.raw, indent=4)) |  | ||||||
| @ -12,7 +12,6 @@ LOCAL = local() | |||||||
| RESPONSE_HEADER_ID = "X-authentik-id" | RESPONSE_HEADER_ID = "X-authentik-id" | ||||||
| KEY_AUTH_VIA = "auth_via" | KEY_AUTH_VIA = "auth_via" | ||||||
| KEY_USER = "user" | KEY_USER = "user" | ||||||
| INTERNAL_HEADER_PREFIX = "X-authentik-internal-" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ImpersonateMiddleware: | class ImpersonateMiddleware: | ||||||
| @ -53,9 +52,9 @@ class RequestIDMiddleware: | |||||||
|             } |             } | ||||||
|         response = self.get_response(request) |         response = self.get_response(request) | ||||||
|         response[RESPONSE_HEADER_ID] = request.request_id |         response[RESPONSE_HEADER_ID] = request.request_id | ||||||
|         if auth_via := LOCAL.authentik.get(KEY_AUTH_VIA, None): |         setattr(response, "ak_context", {}) | ||||||
|             response[INTERNAL_HEADER_PREFIX + KEY_AUTH_VIA] = auth_via |         response.ak_context.update(LOCAL.authentik) | ||||||
|         response[INTERNAL_HEADER_PREFIX + KEY_USER] = request.user.username |         response.ak_context[KEY_USER] = request.user.username | ||||||
|         for key in list(LOCAL.authentik.keys()): |         for key in list(LOCAL.authentik.keys()): | ||||||
|             del LOCAL.authentik[key] |             del LOCAL.authentik[key] | ||||||
|         return response |         return response | ||||||
| @ -66,4 +65,6 @@ def structlog_add_request_id(logger: Logger, method_name: str, event_dict: dict) | |||||||
|     """If threadlocal has authentik defined, add request_id to log""" |     """If threadlocal has authentik defined, add request_id to log""" | ||||||
|     if hasattr(LOCAL, "authentik"): |     if hasattr(LOCAL, "authentik"): | ||||||
|         event_dict.update(LOCAL.authentik) |         event_dict.update(LOCAL.authentik) | ||||||
|  |     if hasattr(LOCAL, "authentik_task"): | ||||||
|  |         event_dict.update(LOCAL.authentik_task) | ||||||
|     return event_dict |     return event_dict | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ | |||||||
| import uuid | import uuid | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| import django.core.validators |  | ||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
| from django.apps.registry import Apps | from django.apps.registry import Apps | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @ -12,6 +11,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor | |||||||
| from django.db.models import Count | from django.db.models import Count | ||||||
|  |  | ||||||
| import authentik.core.models | import authentik.core.models | ||||||
|  | import authentik.lib.models | ||||||
|  |  | ||||||
|  |  | ||||||
| def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||||
| @ -161,7 +161,7 @@ class Migration(migrations.Migration): | |||||||
|             model_name="application", |             model_name="application", | ||||||
|             name="meta_launch_url", |             name="meta_launch_url", | ||||||
|             field=models.TextField( |             field=models.TextField( | ||||||
|                 blank=True, default="", validators=[django.core.validators.URLValidator()] |                 blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()] | ||||||
|             ), |             ), | ||||||
|         ), |         ), | ||||||
|         migrations.RunPython( |         migrations.RunPython( | ||||||
|  | |||||||
| @ -1,8 +1,9 @@ | |||||||
| # Generated by Django 3.2.3 on 2021-06-02 21:51 | # Generated by Django 3.2.3 on 2021-06-02 21:51 | ||||||
|  |  | ||||||
| import django.core.validators |  | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
|  |  | ||||||
|  | import authentik.lib.models | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
| @ -17,7 +18,7 @@ class Migration(migrations.Migration): | |||||||
|             field=models.TextField( |             field=models.TextField( | ||||||
|                 blank=True, |                 blank=True, | ||||||
|                 default="", |                 default="", | ||||||
|                 validators=[django.core.validators.URLValidator()], |                 validators=[authentik.lib.models.DomainlessURLValidator()], | ||||||
|             ), |             ), | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -9,7 +9,6 @@ from deepmerge import always_merger | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import AbstractUser | from django.contrib.auth.models import AbstractUser | ||||||
| from django.contrib.auth.models import UserManager as DjangoUserManager | from django.contrib.auth.models import UserManager as DjangoUserManager | ||||||
| from django.core import validators |  | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.db.models import Q, QuerySet, options | from django.db.models import Q, QuerySet, options | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| @ -26,10 +25,9 @@ from structlog.stdlib import get_logger | |||||||
| from authentik.core.exceptions import PropertyMappingExpressionException | from authentik.core.exceptions import PropertyMappingExpressionException | ||||||
| from authentik.core.signals import password_changed | from authentik.core.signals import password_changed | ||||||
| from authentik.core.types import UILoginButton, UserSettingSerializer | from authentik.core.types import UILoginButton, UserSettingSerializer | ||||||
| from authentik.flows.models import Flow |  | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.lib.models import CreatedUpdatedModel, SerializerModel | from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel | ||||||
| from authentik.lib.utils.http import get_client_ip | from authentik.lib.utils.http import get_client_ip | ||||||
| from authentik.managed.models import ManagedModel | from authentik.managed.models import ManagedModel | ||||||
| from authentik.policies.models import PolicyBindingModel | from authentik.policies.models import PolicyBindingModel | ||||||
| @ -204,7 +202,7 @@ class Provider(SerializerModel): | |||||||
|     name = models.TextField() |     name = models.TextField() | ||||||
|  |  | ||||||
|     authorization_flow = models.ForeignKey( |     authorization_flow = models.ForeignKey( | ||||||
|         Flow, |         "authentik_flows.Flow", | ||||||
|         on_delete=models.CASCADE, |         on_delete=models.CASCADE, | ||||||
|         help_text=_("Flow used when authorizing this provider."), |         help_text=_("Flow used when authorizing this provider."), | ||||||
|         related_name="provider_authorization", |         related_name="provider_authorization", | ||||||
| @ -246,7 +244,7 @@ class Application(PolicyBindingModel): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     meta_launch_url = models.TextField( |     meta_launch_url = models.TextField( | ||||||
|         default="", blank=True, validators=[validators.URLValidator()] |         default="", blank=True, validators=[DomainlessURLValidator()] | ||||||
|     ) |     ) | ||||||
|     # For template applications, this can be set to /static/authentik/applications/* |     # For template applications, this can be set to /static/authentik/applications/* | ||||||
|     meta_icon = models.FileField( |     meta_icon = models.FileField( | ||||||
| @ -264,7 +262,7 @@ class Application(PolicyBindingModel): | |||||||
|         it is returned as-is""" |         it is returned as-is""" | ||||||
|         if not self.meta_icon: |         if not self.meta_icon: | ||||||
|             return None |             return None | ||||||
|         if self.meta_icon.name.startswith("http") or self.meta_icon.name.startswith("/static"): |         if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"): | ||||||
|             return self.meta_icon.name |             return self.meta_icon.name | ||||||
|         return self.meta_icon.url |         return self.meta_icon.url | ||||||
|  |  | ||||||
| @ -325,7 +323,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | |||||||
|     property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True) |     property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True) | ||||||
|  |  | ||||||
|     authentication_flow = models.ForeignKey( |     authentication_flow = models.ForeignKey( | ||||||
|         Flow, |         "authentik_flows.Flow", | ||||||
|         blank=True, |         blank=True, | ||||||
|         null=True, |         null=True, | ||||||
|         default=None, |         default=None, | ||||||
| @ -334,7 +332,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | |||||||
|         related_name="source_authentication", |         related_name="source_authentication", | ||||||
|     ) |     ) | ||||||
|     enrollment_flow = models.ForeignKey( |     enrollment_flow = models.ForeignKey( | ||||||
|         Flow, |         "authentik_flows.Flow", | ||||||
|         blank=True, |         blank=True, | ||||||
|         null=True, |         null=True, | ||||||
|         default=None, |         default=None, | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ LOGGER = get_logger() | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def clean_expired_models(self: MonitoredTask): | def clean_expired_models(self: MonitoredTask): | ||||||
|     """Remove expired objects""" |     """Remove expired objects""" | ||||||
|     messages = [] |     messages = [] | ||||||
| @ -69,7 +69,7 @@ def should_backup() -> bool: | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def backup_database(self: MonitoredTask):  # pragma: no cover | def backup_database(self: MonitoredTask):  # pragma: no cover | ||||||
|     """Database backup""" |     """Database backup""" | ||||||
|     self.result_timeout_hours = 25 |     self.result_timeout_hours = 25 | ||||||
|  | |||||||
| @ -3,7 +3,8 @@ from django.urls import reverse | |||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import Application, User | from authentik.core.models import Application | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.policies.dummy.models import DummyPolicy | from authentik.policies.dummy.models import DummyPolicy | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
|  |  | ||||||
| @ -12,7 +13,7 @@ class TestApplicationsAPI(APITestCase): | |||||||
|     """Test applications API""" |     """Test applications API""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.allowed = Application.objects.create(name="allowed", slug="allowed") |         self.allowed = Application.objects.create(name="allowed", slug="allowed") | ||||||
|         self.denied = Application.objects.create(name="denied", slug="denied") |         self.denied = Application.objects.create(name="denied", slug="denied") | ||||||
|         PolicyBinding.objects.create( |         PolicyBinding.objects.create( | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ from django.utils.encoding import force_str | |||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestAuthenticatedSessionsAPI(APITestCase): | class TestAuthenticatedSessionsAPI(APITestCase): | ||||||
| @ -13,7 +14,7 @@ class TestAuthenticatedSessionsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.other_user = User.objects.create(username="normal-user") |         self.other_user = User.objects.create(username="normal-user") | ||||||
|  |  | ||||||
|     def test_list(self): |     def test_list(self): | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ from django.test.testcases import TestCase | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestImpersonation(TestCase): | class TestImpersonation(TestCase): | ||||||
| @ -13,14 +14,14 @@ class TestImpersonation(TestCase): | |||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.other_user = User.objects.create(username="to-impersonate") |         self.other_user = User.objects.create(username="to-impersonate") | ||||||
|         self.akadmin = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|  |  | ||||||
|     def test_impersonate_simple(self): |     def test_impersonate_simple(self): | ||||||
|         """test simple impersonation and un-impersonation""" |         """test simple impersonation and un-impersonation""" | ||||||
|         # test with an inactive user to ensure that still works |         # test with an inactive user to ensure that still works | ||||||
|         self.other_user.is_active = False |         self.other_user.is_active = False | ||||||
|         self.other_user.save() |         self.other_user.save() | ||||||
|         self.client.force_login(self.akadmin) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|         self.client.get( |         self.client.get( | ||||||
|             reverse( |             reverse( | ||||||
| @ -32,13 +33,13 @@ class TestImpersonation(TestCase): | |||||||
|         response = self.client.get(reverse("authentik_api:user-me")) |         response = self.client.get(reverse("authentik_api:user-me")) | ||||||
|         response_body = loads(response.content.decode()) |         response_body = loads(response.content.decode()) | ||||||
|         self.assertEqual(response_body["user"]["username"], self.other_user.username) |         self.assertEqual(response_body["user"]["username"], self.other_user.username) | ||||||
|         self.assertEqual(response_body["original"]["username"], self.akadmin.username) |         self.assertEqual(response_body["original"]["username"], self.user.username) | ||||||
|  |  | ||||||
|         self.client.get(reverse("authentik_core:impersonate-end")) |         self.client.get(reverse("authentik_core:impersonate-end")) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_api:user-me")) |         response = self.client.get(reverse("authentik_api:user-me")) | ||||||
|         response_body = loads(response.content.decode()) |         response_body = loads(response.content.decode()) | ||||||
|         self.assertEqual(response_body["user"]["username"], self.akadmin.username) |         self.assertEqual(response_body["user"]["username"], self.user.username) | ||||||
|         self.assertNotIn("original", response_body) |         self.assertNotIn("original", response_body) | ||||||
|  |  | ||||||
|     def test_impersonate_denied(self): |     def test_impersonate_denied(self): | ||||||
| @ -46,7 +47,7 @@ class TestImpersonation(TestCase): | |||||||
|         self.client.force_login(self.other_user) |         self.client.force_login(self.other_user) | ||||||
|  |  | ||||||
|         self.client.get( |         self.client.get( | ||||||
|             reverse("authentik_core:impersonate-init", kwargs={"user_id": self.akadmin.pk}) |             reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk}) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_api:user-me")) |         response = self.client.get(reverse("authentik_api:user-me")) | ||||||
|  | |||||||
| @ -49,7 +49,7 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable: | |||||||
|  |  | ||||||
|     def tester(self: TestModels): |     def tester(self: TestModels): | ||||||
|         model_class = None |         model_class = None | ||||||
|         if test_model._meta.abstract: |         if test_model._meta.abstract:  # pragma: no cover | ||||||
|             model_class = test_model.__bases__[0]() |             model_class = test_model.__bases__[0]() | ||||||
|         else: |         else: | ||||||
|             model_class = test_model() |             model_class = test_model() | ||||||
| @ -59,6 +59,6 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable: | |||||||
|  |  | ||||||
|  |  | ||||||
| for model in all_subclasses(Source): | for model in all_subclasses(Source): | ||||||
|     setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model)) |     setattr(TestModels, f"test_source_{model.__name__}", source_tester_factory(model)) | ||||||
| for model in all_subclasses(Provider): | for model in all_subclasses(Provider): | ||||||
|     setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model)) |     setattr(TestModels, f"test_provider_{model.__name__}", provider_tester_factory(model)) | ||||||
|  | |||||||
| @ -6,7 +6,8 @@ from rest_framework.serializers import ValidationError | |||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.api.propertymappings import PropertyMappingSerializer | from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||||
| from authentik.core.models import PropertyMapping, User | from authentik.core.models import PropertyMapping | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestPropertyMappingAPI(APITestCase): | class TestPropertyMappingAPI(APITestCase): | ||||||
| @ -17,7 +18,7 @@ class TestPropertyMappingAPI(APITestCase): | |||||||
|         self.mapping = PropertyMapping.objects.create( |         self.mapping = PropertyMapping.objects.create( | ||||||
|             name="dummy", expression="""return {'foo': 'bar'}""" |             name="dummy", expression="""return {'foo': 'bar'}""" | ||||||
|         ) |         ) | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_test_call(self): |     def test_test_call(self): | ||||||
|  | |||||||
| @ -2,7 +2,8 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import PropertyMapping, User | from authentik.core.models import PropertyMapping | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProvidersAPI(APITestCase): | class TestProvidersAPI(APITestCase): | ||||||
| @ -13,7 +14,7 @@ class TestProvidersAPI(APITestCase): | |||||||
|         self.mapping = PropertyMapping.objects.create( |         self.mapping = PropertyMapping.objects.create( | ||||||
|             name="dummy", expression="""return {'foo': 'bar'}""" |             name="dummy", expression="""return {'foo': 'bar'}""" | ||||||
|         ) |         ) | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_types(self): |     def test_types(self): | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from rest_framework.test import APITestCase | |||||||
|  |  | ||||||
| from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User | from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User | ||||||
| from authentik.core.tasks import clean_expired_models | from authentik.core.tasks import clean_expired_models | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestTokenAPI(APITestCase): | class TestTokenAPI(APITestCase): | ||||||
| @ -16,7 +17,7 @@ class TestTokenAPI(APITestCase): | |||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="testuser") |         self.user = User.objects.create(username="testuser") | ||||||
|         self.admin = User.objects.get(username="akadmin") |         self.admin = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_token_create(self): |     def test_token_create(self): | ||||||
|  | |||||||
| @ -3,7 +3,8 @@ from django.urls.base import reverse | |||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User | from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User | ||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant | ||||||
|  | from authentik.flows.models import FlowDesignation | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
| @ -12,7 +13,7 @@ class TestUsersAPI(APITestCase): | |||||||
|     """Test Users API""" |     """Test Users API""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.admin = User.objects.get(username="akadmin") |         self.admin = create_test_admin_user() | ||||||
|         self.user = User.objects.create(username="test-user") |         self.user = User.objects.create(username="test-user") | ||||||
|  |  | ||||||
|     def test_update_self(self): |     def test_update_self(self): | ||||||
| @ -69,10 +70,8 @@ class TestUsersAPI(APITestCase): | |||||||
|  |  | ||||||
|     def test_recovery(self): |     def test_recovery(self): | ||||||
|         """Test user recovery link (no recovery flow set)""" |         """Test user recovery link (no recovery flow set)""" | ||||||
|         flow = Flow.objects.create( |         flow = create_test_flow(FlowDesignation.RECOVERY) | ||||||
|             name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY |         tenant: Tenant = create_test_tenant() | ||||||
|         ) |  | ||||||
|         tenant: Tenant = Tenant.objects.first() |  | ||||||
|         tenant.flow_recovery = flow |         tenant.flow_recovery = flow | ||||||
|         tenant.save() |         tenant.save() | ||||||
|         self.client.force_login(self.admin) |         self.client.force_login(self.admin) | ||||||
| @ -99,10 +98,8 @@ class TestUsersAPI(APITestCase): | |||||||
|         """Test user recovery link (no email stage)""" |         """Test user recovery link (no email stage)""" | ||||||
|         self.user.email = "foo@bar.baz" |         self.user.email = "foo@bar.baz" | ||||||
|         self.user.save() |         self.user.save() | ||||||
|         flow = Flow.objects.create( |         flow = create_test_flow(designation=FlowDesignation.RECOVERY) | ||||||
|             name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY |         tenant: Tenant = create_test_tenant() | ||||||
|         ) |  | ||||||
|         tenant: Tenant = Tenant.objects.first() |  | ||||||
|         tenant.flow_recovery = flow |         tenant.flow_recovery = flow | ||||||
|         tenant.save() |         tenant.save() | ||||||
|         self.client.force_login(self.admin) |         self.client.force_login(self.admin) | ||||||
| @ -115,10 +112,8 @@ class TestUsersAPI(APITestCase): | |||||||
|         """Test user recovery link""" |         """Test user recovery link""" | ||||||
|         self.user.email = "foo@bar.baz" |         self.user.email = "foo@bar.baz" | ||||||
|         self.user.save() |         self.user.save() | ||||||
|         flow = Flow.objects.create( |         flow = create_test_flow(FlowDesignation.RECOVERY) | ||||||
|             name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY |         tenant: Tenant = create_test_tenant() | ||||||
|         ) |  | ||||||
|         tenant: Tenant = Tenant.objects.first() |  | ||||||
|         tenant.flow_recovery = flow |         tenant.flow_recovery = flow | ||||||
|         tenant.save() |         tenant.save() | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										57
									
								
								authentik/core/tests/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								authentik/core/tests/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | """Test Utils""" | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | from django.utils.text import slugify | ||||||
|  |  | ||||||
|  | from authentik.core.models import Group, User | ||||||
|  | from authentik.crypto.builder import CertificateBuilder | ||||||
|  | from authentik.crypto.models import CertificateKeyPair | ||||||
|  | from authentik.flows.models import Flow, FlowDesignation | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_test_flow(designation: FlowDesignation = FlowDesignation.STAGE_CONFIGURATION) -> Flow: | ||||||
|  |     """Generate a flow that can be used for testing""" | ||||||
|  |     uid = generate_id(10) | ||||||
|  |     return Flow.objects.create( | ||||||
|  |         name=uid, | ||||||
|  |         title=uid, | ||||||
|  |         slug=slugify(uid), | ||||||
|  |         designation=designation, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_test_admin_user(name: Optional[str] = None) -> User: | ||||||
|  |     """Generate a test-admin user""" | ||||||
|  |     uid = generate_id(20) if not name else name | ||||||
|  |     group = Group.objects.create(name=uid, is_superuser=True) | ||||||
|  |     user: User = User.objects.create( | ||||||
|  |         username=uid, | ||||||
|  |         name=uid, | ||||||
|  |         email=f"{uid}@goauthentik.io", | ||||||
|  |     ) | ||||||
|  |     user.set_password(uid) | ||||||
|  |     user.save() | ||||||
|  |     group.users.add(user) | ||||||
|  |     return user | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_test_tenant() -> Tenant: | ||||||
|  |     """Generate a test tenant, removing all other tenants to make sure this one | ||||||
|  |     matches.""" | ||||||
|  |     uid = generate_id(20) | ||||||
|  |     Tenant.objects.all().delete() | ||||||
|  |     return Tenant.objects.create(domain=uid, default=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_test_cert() -> CertificateKeyPair: | ||||||
|  |     """Generate a certificate for testing""" | ||||||
|  |     CertificateKeyPair.objects.filter(name="goauthentik.io").delete() | ||||||
|  |     builder = CertificateBuilder() | ||||||
|  |     builder.common_name = "goauthentik.io" | ||||||
|  |     builder.build( | ||||||
|  |         subject_alt_names=["goauthentik.io"], | ||||||
|  |         validity_days=360, | ||||||
|  |     ) | ||||||
|  |     return builder.save() | ||||||
| @ -20,6 +20,7 @@ from authentik.api.decorators import permission_required | |||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.crypto.builder import CertificateBuilder | from authentik.crypto.builder import CertificateBuilder | ||||||
|  | from authentik.crypto.managed import MANAGED_KEY | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
| @ -141,9 +142,11 @@ class CertificateKeyPairFilter(FilterSet): | |||||||
| class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): | class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): | ||||||
|     """CertificateKeyPair Viewset""" |     """CertificateKeyPair Viewset""" | ||||||
|  |  | ||||||
|     queryset = CertificateKeyPair.objects.exclude(managed__isnull=False) |     queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY) | ||||||
|     serializer_class = CertificateKeyPairSerializer |     serializer_class = CertificateKeyPairSerializer | ||||||
|     filterset_class = CertificateKeyPairFilter |     filterset_class = CertificateKeyPairFilter | ||||||
|  |     ordering = ["name"] | ||||||
|  |     search_fields = ["name"] | ||||||
|  |  | ||||||
|     @permission_required(None, ["authentik_crypto.add_certificatekeypair"]) |     @permission_required(None, ["authentik_crypto.add_certificatekeypair"]) | ||||||
|     @extend_schema( |     @extend_schema( | ||||||
| @ -189,7 +192,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): | |||||||
|             secret=certificate, |             secret=certificate, | ||||||
|             type="certificate", |             type="certificate", | ||||||
|         ).from_http(request) |         ).from_http(request) | ||||||
|         if "download" in request._request.GET: |         if "download" in request.query_params: | ||||||
|             # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html |             # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html | ||||||
|             response = HttpResponse( |             response = HttpResponse( | ||||||
|                 certificate.certificate_data, content_type="application/x-pem-file" |                 certificate.certificate_data, content_type="application/x-pem-file" | ||||||
| @ -220,7 +223,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): | |||||||
|             secret=certificate, |             secret=certificate, | ||||||
|             type="private_key", |             type="private_key", | ||||||
|         ).from_http(request) |         ).from_http(request) | ||||||
|         if "download" in request._request.GET: |         if "download" in request.query_params: | ||||||
|             # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html |             # Mime type from https://pki-tutorial.readthedocs.io/en/latest/mime.html | ||||||
|             response = HttpResponse(certificate.key_data, content_type="application/x-pem-file") |             response = HttpResponse(certificate.key_data, content_type="application/x-pem-file") | ||||||
|             response[ |             response[ | ||||||
|  | |||||||
| @ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig): | |||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|         import_module("authentik.crypto.managed") |         import_module("authentik.crypto.managed") | ||||||
|  |         import_module("authentik.crypto.tasks") | ||||||
|  | |||||||
| @ -55,7 +55,7 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel): | |||||||
|     @property |     @property | ||||||
|     def private_key(self) -> Optional[RSAPrivateKey]: |     def private_key(self) -> Optional[RSAPrivateKey]: | ||||||
|         """Get python cryptography PrivateKey instance""" |         """Get python cryptography PrivateKey instance""" | ||||||
|         if not self._private_key and self._private_key != "": |         if not self._private_key and self.key_data != "": | ||||||
|             try: |             try: | ||||||
|                 self._private_key = load_pem_private_key( |                 self._private_key = load_pem_private_key( | ||||||
|                     str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])), |                     str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])), | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								authentik/crypto/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								authentik/crypto/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | """Crypto task Settings""" | ||||||
|  | from celery.schedules import crontab | ||||||
|  |  | ||||||
|  | CELERY_BEAT_SCHEDULE = { | ||||||
|  |     "crypto_certificate_discovery": { | ||||||
|  |         "task": "authentik.crypto.tasks.certificate_discovery", | ||||||
|  |         "schedule": crontab(minute="*/5"), | ||||||
|  |         "options": {"queue": "authentik_scheduled"}, | ||||||
|  |     }, | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								authentik/crypto/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								authentik/crypto/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | """Crypto tasks""" | ||||||
|  | from glob import glob | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.crypto.models import CertificateKeyPair | ||||||
|  | from authentik.events.monitored_tasks import ( | ||||||
|  |     MonitoredTask, | ||||||
|  |     TaskResult, | ||||||
|  |     TaskResultStatus, | ||||||
|  |     prefill_task, | ||||||
|  | ) | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
|  | @prefill_task | ||||||
|  | def certificate_discovery(self: MonitoredTask): | ||||||
|  |     """Discover and update certificates form the filesystem""" | ||||||
|  |     certs = {} | ||||||
|  |     private_keys = {} | ||||||
|  |     discovered = 0 | ||||||
|  |     for file in glob(CONFIG.y("cert_discovery_dir") + "/**", recursive=True): | ||||||
|  |         path = Path(file) | ||||||
|  |         if not path.exists(): | ||||||
|  |             continue | ||||||
|  |         if path.is_dir(): | ||||||
|  |             continue | ||||||
|  |         # Support certbot's directory structure | ||||||
|  |         if path.name in ["fullchain.pem", "privkey.pem"]: | ||||||
|  |             cert_name = path.parent.name | ||||||
|  |         else: | ||||||
|  |             cert_name = path.name.replace(path.suffix, "") | ||||||
|  |         try: | ||||||
|  |             with open(path, "r+", encoding="utf-8") as _file: | ||||||
|  |                 body = _file.read() | ||||||
|  |                 if "BEGIN RSA PRIVATE KEY" in body: | ||||||
|  |                     private_keys[cert_name] = body | ||||||
|  |                 else: | ||||||
|  |                     certs[cert_name] = body | ||||||
|  |         except OSError as exc: | ||||||
|  |             LOGGER.warning("Failed to open file", exc=exc, file=path) | ||||||
|  |         discovered += 1 | ||||||
|  |     for name, cert_data in certs.items(): | ||||||
|  |         cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first() | ||||||
|  |         if not cert: | ||||||
|  |             cert = CertificateKeyPair( | ||||||
|  |                 name=name, | ||||||
|  |                 managed=MANAGED_DISCOVERED % name, | ||||||
|  |             ) | ||||||
|  |         dirty = False | ||||||
|  |         if cert.certificate_data != cert_data: | ||||||
|  |             cert.certificate_data = cert_data | ||||||
|  |             dirty = True | ||||||
|  |         if name in private_keys: | ||||||
|  |             if cert.key_data == private_keys[name]: | ||||||
|  |                 cert.key_data = private_keys[name] | ||||||
|  |                 dirty = True | ||||||
|  |         if dirty: | ||||||
|  |             cert.save() | ||||||
|  |     self.set_status( | ||||||
|  |         TaskResult( | ||||||
|  |             TaskResultStatus.SUCCESSFUL, | ||||||
|  |             messages=[_("Successfully imported %(count)d files." % {"count": discovered})], | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
| @ -1,25 +1,37 @@ | |||||||
| """Crypto tests""" | """Crypto tests""" | ||||||
| import datetime | import datetime | ||||||
|  | from os import makedirs | ||||||
|  | from tempfile import TemporaryDirectory | ||||||
|  |  | ||||||
| from django.test import TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.api.used_by import DeleteAction | from authentik.core.api.used_by import DeleteAction | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.crypto.api import CertificateKeyPairSerializer | from authentik.crypto.api import CertificateKeyPairSerializer | ||||||
| from authentik.crypto.builder import CertificateBuilder | from authentik.crypto.builder import CertificateBuilder | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.flows.models import Flow | from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.generators import generate_key | from authentik.lib.generators import generate_key | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider | from authentik.providers.oauth2.models import OAuth2Provider | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestCrypto(TestCase): | class TestCrypto(APITestCase): | ||||||
|     """Test Crypto validation""" |     """Test Crypto validation""" | ||||||
|  |  | ||||||
|  |     def test_model_private(self): | ||||||
|  |         """Test model private key""" | ||||||
|  |         cert = CertificateKeyPair.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             certificate_data="foo", | ||||||
|  |             key_data="foo", | ||||||
|  |         ) | ||||||
|  |         self.assertIsNone(cert.private_key) | ||||||
|  |  | ||||||
|     def test_serializer(self): |     def test_serializer(self): | ||||||
|         """Test API Validation""" |         """Test API Validation""" | ||||||
|         keypair = CertificateKeyPair.objects.first() |         keypair = create_test_cert() | ||||||
|         self.assertTrue( |         self.assertTrue( | ||||||
|             CertificateKeyPairSerializer( |             CertificateKeyPairSerializer( | ||||||
|                 data={ |                 data={ | ||||||
| @ -54,10 +66,38 @@ class TestCrypto(TestCase): | |||||||
|         self.assertEqual(instance.name, "test-cert") |         self.assertEqual(instance.name, "test-cert") | ||||||
|         self.assertEqual((instance.certificate.not_valid_after - now).days, 2) |         self.assertEqual((instance.certificate.not_valid_after - now).days, 2) | ||||||
|  |  | ||||||
|  |     def test_builder_api(self): | ||||||
|  |         """Test Builder (via API)""" | ||||||
|  |         self.client.force_login(create_test_admin_user()) | ||||||
|  |         self.client.post( | ||||||
|  |             reverse("authentik_api:certificatekeypair-generate"), | ||||||
|  |             data={"common_name": "foo", "subject_alt_name": "bar,baz", "validity_days": 3}, | ||||||
|  |         ) | ||||||
|  |         self.assertTrue(CertificateKeyPair.objects.filter(name="foo").exists()) | ||||||
|  |  | ||||||
|  |     def test_builder_api_invalid(self): | ||||||
|  |         """Test Builder (via API) (invalid)""" | ||||||
|  |         self.client.force_login(create_test_admin_user()) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_api:certificatekeypair-generate"), | ||||||
|  |             data={}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 400) | ||||||
|  |  | ||||||
|  |     def test_list(self): | ||||||
|  |         """Test API List""" | ||||||
|  |         self.client.force_login(create_test_admin_user()) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_api:certificatekeypair-list", | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(200, response.status_code) | ||||||
|  |  | ||||||
|     def test_certificate_download(self): |     def test_certificate_download(self): | ||||||
|         """Test certificate export (download)""" |         """Test certificate export (download)""" | ||||||
|         self.client.force_login(User.objects.get(username="akadmin")) |         self.client.force_login(create_test_admin_user()) | ||||||
|         keypair = CertificateKeyPair.objects.first() |         keypair = create_test_cert() | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_api:certificatekeypair-view-certificate", |                 "authentik_api:certificatekeypair-view-certificate", | ||||||
| @ -77,8 +117,8 @@ class TestCrypto(TestCase): | |||||||
|  |  | ||||||
|     def test_private_key_download(self): |     def test_private_key_download(self): | ||||||
|         """Test private_key export (download)""" |         """Test private_key export (download)""" | ||||||
|         self.client.force_login(User.objects.get(username="akadmin")) |         self.client.force_login(create_test_admin_user()) | ||||||
|         keypair = CertificateKeyPair.objects.first() |         keypair = create_test_cert() | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_api:certificatekeypair-view-private-key", |                 "authentik_api:certificatekeypair-view-private-key", | ||||||
| @ -98,15 +138,15 @@ class TestCrypto(TestCase): | |||||||
|  |  | ||||||
|     def test_used_by(self): |     def test_used_by(self): | ||||||
|         """Test used_by endpoint""" |         """Test used_by endpoint""" | ||||||
|         self.client.force_login(User.objects.get(username="akadmin")) |         self.client.force_login(create_test_admin_user()) | ||||||
|         keypair = CertificateKeyPair.objects.first() |         keypair = create_test_cert() | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             client_secret=generate_key(), |             client_secret=generate_key(), | ||||||
|             authorization_flow=Flow.objects.first(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://localhost", |             redirect_uris="http://localhost", | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |             rsa_key=keypair, | ||||||
|         ) |         ) | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse( |             reverse( | ||||||
| @ -127,3 +167,33 @@ class TestCrypto(TestCase): | |||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def test_discovery(self): | ||||||
|  |         """Test certificate discovery""" | ||||||
|  |         builder = CertificateBuilder() | ||||||
|  |         builder.common_name = "test-cert" | ||||||
|  |         with self.assertRaises(ValueError): | ||||||
|  |             builder.save() | ||||||
|  |         builder.build( | ||||||
|  |             subject_alt_names=[], | ||||||
|  |             validity_days=3, | ||||||
|  |         ) | ||||||
|  |         with TemporaryDirectory() as temp_dir: | ||||||
|  |             with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert: | ||||||
|  |                 _cert.write(builder.certificate) | ||||||
|  |             with open(f"{temp_dir}/foo.key", "w+", encoding="utf-8") as _key: | ||||||
|  |                 _key.write(builder.private_key) | ||||||
|  |             makedirs(f"{temp_dir}/foo.bar", exist_ok=True) | ||||||
|  |             with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert: | ||||||
|  |                 _cert.write(builder.certificate) | ||||||
|  |             with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key: | ||||||
|  |                 _key.write(builder.private_key) | ||||||
|  |             with CONFIG.patch("cert_discovery_dir", temp_dir): | ||||||
|  |                 # pyright: reportGeneralTypeIssues=false | ||||||
|  |                 certificate_discovery()  # pylint: disable=no-value-for-parameter | ||||||
|  |         self.assertTrue( | ||||||
|  |             CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo").exists() | ||||||
|  |         ) | ||||||
|  |         self.assertTrue( | ||||||
|  |             CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists() | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import uuid | |||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| from typing import Iterable | from typing import Iterable | ||||||
|  |  | ||||||
| import django.core.validators |  | ||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
| from django.apps.registry import Apps | from django.apps.registry import Apps | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @ -12,6 +11,7 @@ from django.db import migrations, models | |||||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||||
|  |  | ||||||
| import authentik.events.models | import authentik.events.models | ||||||
|  | import authentik.lib.models | ||||||
| from authentik.events.models import EventAction, NotificationSeverity, TransportMode | from authentik.events.models import EventAction, NotificationSeverity, TransportMode | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -826,6 +826,8 @@ class Migration(migrations.Migration): | |||||||
|         migrations.AlterField( |         migrations.AlterField( | ||||||
|             model_name="notificationtransport", |             model_name="notificationtransport", | ||||||
|             name="webhook_url", |             name="webhook_url", | ||||||
|             field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]), |             field=models.TextField( | ||||||
|  |                 blank=True, validators=[authentik.lib.models.DomainlessURLValidator()] | ||||||
|  |             ), | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -1,8 +1,9 @@ | |||||||
| # Generated by Django 3.2.7 on 2021-10-04 15:31 | # Generated by Django 3.2.7 on 2021-10-04 15:31 | ||||||
|  |  | ||||||
| import django.core.validators |  | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
|  |  | ||||||
|  | import authentik.lib.models | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
| @ -14,6 +15,8 @@ class Migration(migrations.Migration): | |||||||
|         migrations.AlterField( |         migrations.AlterField( | ||||||
|             model_name="notificationtransport", |             model_name="notificationtransport", | ||||||
|             name="webhook_url", |             name="webhook_url", | ||||||
|             field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]), |             field=models.TextField( | ||||||
|  |                 blank=True, validators=[authentik.lib.models.DomainlessURLValidator()] | ||||||
|  |             ), | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Type, Union | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.validators import URLValidator |  | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from django.http.request import QueryDict | from django.http.request import QueryDict | ||||||
| @ -20,6 +19,7 @@ from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION | |||||||
| from authentik.core.models import ExpiringModel, Group, PropertyMapping, User | from authentik.core.models import ExpiringModel, Group, PropertyMapping, User | ||||||
| from authentik.events.geo import GEOIP_READER | from authentik.events.geo import GEOIP_READER | ||||||
| from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict | from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict | ||||||
|  | from authentik.lib.models import DomainlessURLValidator | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.lib.utils.http import get_client_ip, get_http_session | from authentik.lib.utils.http import get_client_ip, get_http_session | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| @ -224,7 +224,7 @@ class NotificationTransport(models.Model): | |||||||
|     name = models.TextField(unique=True) |     name = models.TextField(unique=True) | ||||||
|     mode = models.TextField(choices=TransportMode.choices) |     mode = models.TextField(choices=TransportMode.choices) | ||||||
|  |  | ||||||
|     webhook_url = models.TextField(blank=True, validators=[URLValidator()]) |     webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()]) | ||||||
|     webhook_mapping = models.ForeignKey( |     webhook_mapping = models.ForeignKey( | ||||||
|         "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None |         "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -112,30 +112,6 @@ class TaskInfo: | |||||||
|         cache.set(key, self, timeout=timeout_hours * 60 * 60) |         cache.set(key, self, timeout=timeout_hours * 60 * 60) | ||||||
|  |  | ||||||
|  |  | ||||||
| def prefill_task(): |  | ||||||
|     """Ensure a task's details are always in cache, so it can always be triggered via API""" |  | ||||||
|  |  | ||||||
|     def inner_wrap(func): |  | ||||||
|         status = TaskInfo.by_name(func.__name__) |  | ||||||
|         if status: |  | ||||||
|             return func |  | ||||||
|         TaskInfo( |  | ||||||
|             task_name=func.__name__, |  | ||||||
|             task_description=func.__doc__, |  | ||||||
|             result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]), |  | ||||||
|             task_call_module=func.__module__, |  | ||||||
|             task_call_func=func.__name__, |  | ||||||
|             # We don't have real values for these attributes but they cannot be null |  | ||||||
|             start_timestamp=default_timer(), |  | ||||||
|             finish_timestamp=default_timer(), |  | ||||||
|             finish_time=datetime.now(), |  | ||||||
|         ).save(86400) |  | ||||||
|         LOGGER.debug("prefilled task", task_name=func.__name__) |  | ||||||
|         return func |  | ||||||
|  |  | ||||||
|     return inner_wrap |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MonitoredTask(Task): | class MonitoredTask(Task): | ||||||
|     """Task which can save its state to the cache""" |     """Task which can save its state to the cache""" | ||||||
|  |  | ||||||
| @ -210,5 +186,21 @@ class MonitoredTask(Task): | |||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
| for task in TaskInfo.all().values(): | def prefill_task(func): | ||||||
|     task.set_prom_metrics() |     """Ensure a task's details are always in cache, so it can always be triggered via API""" | ||||||
|  |     status = TaskInfo.by_name(func.__name__) | ||||||
|  |     if status: | ||||||
|  |         return func | ||||||
|  |     TaskInfo( | ||||||
|  |         task_name=func.__name__, | ||||||
|  |         task_description=func.__doc__, | ||||||
|  |         result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]), | ||||||
|  |         task_call_module=func.__module__, | ||||||
|  |         task_call_func=func.__name__, | ||||||
|  |         # We don't have real values for these attributes but they cannot be null | ||||||
|  |         start_timestamp=default_timer(), | ||||||
|  |         finish_timestamp=default_timer(), | ||||||
|  |         finish_time=datetime.now(), | ||||||
|  |     ).save(86400) | ||||||
|  |     LOGGER.debug("prefilled task", task_name=func.__name__) | ||||||
|  |     return func | ||||||
|  | |||||||
| @ -3,14 +3,14 @@ from threading import Thread | |||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
|  |  | ||||||
| from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed | from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed | ||||||
| from django.db.models.signals import post_save | from django.db.models.signals import post_save, pre_delete | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.core.signals import password_changed | from authentik.core.signals import password_changed | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.events.tasks import event_notification_handler | from authentik.events.tasks import event_notification_handler, gdpr_cleanup | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.stages.invitation.models import Invitation | from authentik.stages.invitation.models import Invitation | ||||||
| @ -108,3 +108,10 @@ def on_password_changed(sender, user: User, password: str, **_): | |||||||
| def event_post_save_notification(sender, instance: Event, **_): | def event_post_save_notification(sender, instance: Event, **_): | ||||||
|     """Start task to check if any policies trigger an notification on this event""" |     """Start task to check if any policies trigger an notification on this event""" | ||||||
|     event_notification_handler.delay(instance.event_uuid.hex) |     event_notification_handler.delay(instance.event_uuid.hex) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(pre_delete, sender=User) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def event_user_pre_delete_cleanup(sender, instance: User, **_): | ||||||
|  |     """If gdpr_compliance is enabled, remove all the user's events""" | ||||||
|  |     gdpr_cleanup.delay(instance.pk) | ||||||
|  | |||||||
| @ -106,3 +106,11 @@ def notification_transport(self: MonitoredTask, notification_pk: int, transport_ | |||||||
|     except NotificationTransportError as exc: |     except NotificationTransportError as exc: | ||||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) |         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||||
|         raise exc |         raise exc | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task() | ||||||
|  | def gdpr_cleanup(user_pk: int): | ||||||
|  |     """cleanup events from gdpr_compliance""" | ||||||
|  |     events = Event.objects.filter(user__pk=user_pk) | ||||||
|  |     LOGGER.debug("GDPR cleanup, removing events from user", events=events.count()) | ||||||
|  |     events.delete() | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.events.models import ( | from authentik.events.models import ( | ||||||
|     Event, |     Event, | ||||||
|     EventAction, |     EventAction, | ||||||
| @ -17,7 +17,7 @@ class TestEventsAPI(APITestCase): | |||||||
|     """Test Event API""" |     """Test Event API""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_top_n(self): |     def test_top_n(self): | ||||||
|  | |||||||
| @ -3,7 +3,8 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import Application, User | from authentik.core.models import Application | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -12,7 +13,7 @@ class TestEventsMiddleware(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_create(self): |     def test_create(self): | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "component", |             "component", | ||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|  |             "meta_model_name", | ||||||
|             "flow_set", |             "flow_set", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ from django.test import RequestFactory | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||||
|  |  | ||||||
| @ -68,7 +68,7 @@ class Command(BaseCommand):  # pragma: no cover | |||||||
|     def benchmark_flows(self, proc_count): |     def benchmark_flows(self, proc_count): | ||||||
|         """Get full recovery link""" |         """Get full recovery link""" | ||||||
|         flow = Flow.objects.get(slug="default-authentication-flow") |         flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
|         user = User.objects.get(username="akadmin") |         user = create_test_admin_user() | ||||||
|         manager = Manager() |         manager = Manager() | ||||||
|         return_dict = manager.dict() |         return_dict = manager.dict() | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								authentik/flows/migrations/0020_flowtoken.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								authentik/flows/migrations/0020_flowtoken.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | # Generated by Django 3.2.9 on 2021-12-05 13:50 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0018_auto_20210330_1345_squashed_0028_alter_token_intent"), | ||||||
|  |         ( | ||||||
|  |             "authentik_flows", | ||||||
|  |             "0019_alter_flow_background_squashed_0024_alter_flow_compatibility_mode", | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="FlowToken", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "token_ptr", | ||||||
|  |                     models.OneToOneField( | ||||||
|  |                         auto_created=True, | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         parent_link=True, | ||||||
|  |                         primary_key=True, | ||||||
|  |                         serialize=False, | ||||||
|  |                         to="authentik_core.token", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ("_plan", models.TextField()), | ||||||
|  |                 ( | ||||||
|  |                     "flow", | ||||||
|  |                     models.ForeignKey( | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.flow" | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "Flow Token", | ||||||
|  |                 "verbose_name_plural": "Flow Tokens", | ||||||
|  |             }, | ||||||
|  |             bases=("authentik_core.token",), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -1,4 +1,6 @@ | |||||||
| """Flow models""" | """Flow models""" | ||||||
|  | from base64 import b64decode, b64encode | ||||||
|  | from pickle import dumps, loads  # nosec | ||||||
| from typing import TYPE_CHECKING, Optional, Type | from typing import TYPE_CHECKING, Optional, Type | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| @ -9,11 +11,13 @@ from model_utils.managers import InheritanceManager | |||||||
| from rest_framework.serializers import BaseSerializer | from rest_framework.serializers import BaseSerializer | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.core.models import Token | ||||||
| from authentik.core.types import UserSettingSerializer | from authentik.core.types import UserSettingSerializer | ||||||
| from authentik.lib.models import InheritanceForeignKey, SerializerModel | from authentik.lib.models import InheritanceForeignKey, SerializerModel | ||||||
| from authentik.policies.models import PolicyBindingModel | from authentik.policies.models import PolicyBindingModel | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|  |     from authentik.flows.planner import FlowPlan | ||||||
|     from authentik.flows.stage import StageView |     from authentik.flows.stage import StageView | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| @ -260,3 +264,30 @@ class ConfigurableStage(models.Model): | |||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         abstract = True |         abstract = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FlowToken(Token): | ||||||
|  |     """Subclass of a standard Token, stores the currently active flow plan upon creation. | ||||||
|  |     Can be used to later resume a flow.""" | ||||||
|  |  | ||||||
|  |     flow = models.ForeignKey(Flow, on_delete=models.CASCADE) | ||||||
|  |     _plan = models.TextField() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def pickle(plan) -> str: | ||||||
|  |         """Pickle into string""" | ||||||
|  |         data = dumps(plan) | ||||||
|  |         return b64encode(data).decode() | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def plan(self) -> "FlowPlan": | ||||||
|  |         """Load Flow plan from pickled version""" | ||||||
|  |         return loads(b64decode(self._plan.encode()))  # nosec | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"Flow Token {super.__str__()}" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         verbose_name = _("Flow Token") | ||||||
|  |         verbose_name_plural = _("Flow Tokens") | ||||||
|  | |||||||
| @ -24,6 +24,9 @@ PLAN_CONTEXT_SSO = "is_sso" | |||||||
| PLAN_CONTEXT_REDIRECT = "redirect" | PLAN_CONTEXT_REDIRECT = "redirect" | ||||||
| PLAN_CONTEXT_APPLICATION = "application" | PLAN_CONTEXT_APPLICATION = "application" | ||||||
| PLAN_CONTEXT_SOURCE = "source" | PLAN_CONTEXT_SOURCE = "source" | ||||||
|  | # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan | ||||||
|  | # was restored. | ||||||
|  | PLAN_CONTEXT_IS_RESTORED = "is_restored" | ||||||
| GAUGE_FLOWS_CACHED = UpdatingGauge( | GAUGE_FLOWS_CACHED = UpdatingGauge( | ||||||
|     "authentik_flows_cached", |     "authentik_flows_cached", | ||||||
|     "Cached flows", |     "Cached flows", | ||||||
|  | |||||||
| @ -149,7 +149,7 @@ class ChallengeStageView(StageView): | |||||||
|                 ) |                 ) | ||||||
|         challenge_response.initial_data["response_errors"] = full_errors |         challenge_response.initial_data["response_errors"] = full_errors | ||||||
|         if not challenge_response.is_valid(): |         if not challenge_response.is_valid(): | ||||||
|             LOGGER.warning( |             LOGGER.error( | ||||||
|                 "f(ch): invalid challenge response", |                 "f(ch): invalid challenge response", | ||||||
|                 binding=self.executor.current_binding, |                 binding=self.executor.current_binding, | ||||||
|                 errors=challenge_response.errors, |                 errors=challenge_response.errors, | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.flows.api.stages import StageSerializer, StageViewSet | from authentik.flows.api.stages import StageSerializer, StageViewSet | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage | ||||||
| from authentik.policies.dummy.models import DummyPolicy | from authentik.policies.dummy.models import DummyPolicy | ||||||
| @ -47,7 +47,7 @@ class TestFlowsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def test_api_diagram(self): |     def test_api_diagram(self): | ||||||
|         """Test flow diagram.""" |         """Test flow diagram.""" | ||||||
|         user = User.objects.get(username="akadmin") |         user = create_test_admin_user() | ||||||
|         self.client.force_login(user) |         self.client.force_login(user) | ||||||
|  |  | ||||||
|         flow = Flow.objects.create( |         flow = Flow.objects.create( | ||||||
| @ -77,7 +77,7 @@ class TestFlowsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def test_api_diagram_no_stages(self): |     def test_api_diagram_no_stages(self): | ||||||
|         """Test flow diagram with no stages.""" |         """Test flow diagram with no stages.""" | ||||||
|         user = User.objects.get(username="akadmin") |         user = create_test_admin_user() | ||||||
|         self.client.force_login(user) |         self.client.force_login(user) | ||||||
|  |  | ||||||
|         flow = Flow.objects.create( |         flow = Flow.objects.create( | ||||||
| @ -93,7 +93,7 @@ class TestFlowsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def test_types(self): |     def test_types(self): | ||||||
|         """Test Stage's types endpoint""" |         """Test Stage's types endpoint""" | ||||||
|         user = User.objects.get(username="akadmin") |         user = create_test_admin_user() | ||||||
|         self.client.force_login(user) |         self.client.force_login(user) | ||||||
|  |  | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from django.test.client import RequestFactory | |||||||
| from django.urls.base import reverse | from django.urls.base import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction | ||||||
| from authentik.stages.dummy.models import DummyStage | from authentik.stages.dummy.models import DummyStage | ||||||
| @ -18,7 +18,7 @@ class TestFlowInspector(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.request_factory = RequestFactory() |         self.request_factory = RequestFactory() | ||||||
|         self.admin = User.objects.get(username="akadmin") |         self.admin = create_test_admin_user() | ||||||
|         self.client.force_login(self.admin) |         self.client.force_login(self.admin) | ||||||
|  |  | ||||||
|     def test(self): |     def test(self): | ||||||
| @ -77,7 +77,7 @@ class TestFlowInspector(APITestCase): | |||||||
|  |  | ||||||
|         self.client.post( |         self.client.post( | ||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|             {"uid_field": "akadmin"}, |             {"uid_field": self.admin.username}, | ||||||
|             follow=True, |             follow=True, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @ -89,5 +89,5 @@ class TestFlowInspector(APITestCase): | |||||||
|         self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident") |         self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident") | ||||||
|         self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2") |         self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             content["current_plan"]["plan_context"]["pending_user"]["username"], "akadmin" |             content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -17,13 +17,13 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable: | |||||||
|  |  | ||||||
|     def tester(self: TestModels): |     def tester(self: TestModels): | ||||||
|         model_class = None |         model_class = None | ||||||
|         if test_model._meta.abstract: |         if test_model._meta.abstract:  # pragma: no cover | ||||||
|             model_class = test_model.__bases__[0]() |             model_class = test_model.__bases__[0]() | ||||||
|         else: |         else: | ||||||
|             model_class = test_model() |             model_class = test_model() | ||||||
|         self.assertTrue(issubclass(model_class.type, StageView)) |         self.assertTrue(issubclass(model_class.type, StageView)) | ||||||
|         self.assertIsNotNone(test_model.component) |         self.assertIsNotNone(test_model.component) | ||||||
|         _ = test_model.ui_user_settings |         _ = model_class.ui_user_settings | ||||||
|  |  | ||||||
|     return tester |     return tester | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
|  | from authentik.core.tests.utils import create_test_flow | ||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| from authentik.flows.planner import FlowPlan | from authentik.flows.planner import FlowPlan | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| @ -12,9 +13,8 @@ class TestHelperView(TestCase): | |||||||
|  |  | ||||||
|     def test_default_view(self): |     def test_default_view(self): | ||||||
|         """Test that ToDefaultFlow returns the expected URL""" |         """Test that ToDefaultFlow returns the expected URL""" | ||||||
|         flow = Flow.objects.filter( |         Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete() | ||||||
|             designation=FlowDesignation.INVALIDATION, |         flow = create_test_flow(FlowDesignation.INVALIDATION) | ||||||
|         ).first() |  | ||||||
|         response = self.client.get( |         response = self.client.get( | ||||||
|             reverse("authentik_flows:default-invalidation"), |             reverse("authentik_flows:default-invalidation"), | ||||||
|         ) |         ) | ||||||
| @ -24,9 +24,8 @@ class TestHelperView(TestCase): | |||||||
|  |  | ||||||
|     def test_default_view_invalid_plan(self): |     def test_default_view_invalid_plan(self): | ||||||
|         """Test that ToDefaultFlow returns the expected URL (with an invalid plan)""" |         """Test that ToDefaultFlow returns the expected URL (with an invalid plan)""" | ||||||
|         flow = Flow.objects.filter( |         Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete() | ||||||
|             designation=FlowDesignation.INVALIDATION, |         flow = create_test_flow(FlowDesignation.INVALIDATION) | ||||||
|         ).first() |  | ||||||
|         plan = FlowPlan(flow_pk=flow.pk.hex + "aa") |         plan = FlowPlan(flow_pk=flow.pk.hex + "aa") | ||||||
|         session = self.client.session |         session = self.client.session | ||||||
|         session[SESSION_KEY_PLAN] = plan |         session[SESSION_KEY_PLAN] = plan | ||||||
|  | |||||||
| @ -34,8 +34,16 @@ from authentik.flows.challenge import ( | |||||||
|     WithUserInfoChallenge, |     WithUserInfoChallenge, | ||||||
| ) | ) | ||||||
| from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||||
| from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, FlowStageBinding, Stage | from authentik.flows.models import ( | ||||||
|  |     ConfigurableStage, | ||||||
|  |     Flow, | ||||||
|  |     FlowDesignation, | ||||||
|  |     FlowStageBinding, | ||||||
|  |     FlowToken, | ||||||
|  |     Stage, | ||||||
|  | ) | ||||||
| from authentik.flows.planner import ( | from authentik.flows.planner import ( | ||||||
|  |     PLAN_CONTEXT_IS_RESTORED, | ||||||
|     PLAN_CONTEXT_PENDING_USER, |     PLAN_CONTEXT_PENDING_USER, | ||||||
|     PLAN_CONTEXT_REDIRECT, |     PLAN_CONTEXT_REDIRECT, | ||||||
|     FlowPlan, |     FlowPlan, | ||||||
| @ -53,7 +61,9 @@ NEXT_ARG_NAME = "next" | |||||||
| SESSION_KEY_PLAN = "authentik_flows_plan" | SESSION_KEY_PLAN = "authentik_flows_plan" | ||||||
| SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre" | SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre" | ||||||
| SESSION_KEY_GET = "authentik_flows_get" | SESSION_KEY_GET = "authentik_flows_get" | ||||||
|  | SESSION_KEY_POST = "authentik_flows_post" | ||||||
| SESSION_KEY_HISTORY = "authentik_flows_history" | SESSION_KEY_HISTORY = "authentik_flows_history" | ||||||
|  | QS_KEY_TOKEN = "flow_token"  # nosec | ||||||
|  |  | ||||||
|  |  | ||||||
| def challenge_types(): | def challenge_types(): | ||||||
| @ -126,8 +136,31 @@ class FlowExecutorView(APIView): | |||||||
|         message = exc.__doc__ if exc.__doc__ else str(exc) |         message = exc.__doc__ if exc.__doc__ else str(exc) | ||||||
|         return self.stage_invalid(error_message=message) |         return self.stage_invalid(error_message=message) | ||||||
|  |  | ||||||
|  |     def _check_flow_token(self, get_params: QueryDict): | ||||||
|  |         """Check if the user is using a flow token to restore a plan""" | ||||||
|  |         tokens = FlowToken.filter_not_expired(key=get_params[QS_KEY_TOKEN]) | ||||||
|  |         if not tokens.exists(): | ||||||
|  |             return False | ||||||
|  |         token: FlowToken = tokens.first() | ||||||
|  |         try: | ||||||
|  |             plan = token.plan | ||||||
|  |         except (AttributeError, EOFError, ImportError, IndexError) as exc: | ||||||
|  |             LOGGER.warning("f(exec): Failed to restore token plan", exc=exc) | ||||||
|  |         finally: | ||||||
|  |             token.delete() | ||||||
|  |         if not isinstance(plan, FlowPlan): | ||||||
|  |             return None | ||||||
|  |         plan.context[PLAN_CONTEXT_IS_RESTORED] = True | ||||||
|  |         self._logger.debug("f(exec): restored flow plan from token", plan=plan) | ||||||
|  |         return plan | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument, too-many-return-statements |     # pylint: disable=unused-argument, too-many-return-statements | ||||||
|     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: |     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: | ||||||
|  |         get_params = QueryDict(request.GET.get("query", "")) | ||||||
|  |         if QS_KEY_TOKEN in get_params: | ||||||
|  |             plan = self._check_flow_token(get_params) | ||||||
|  |             if plan: | ||||||
|  |                 self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|         # Early check if there's 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: |         if SESSION_KEY_PLAN in self.request.session: | ||||||
|             self.plan = self.request.session[SESSION_KEY_PLAN] |             self.plan = self.request.session[SESSION_KEY_PLAN] | ||||||
| @ -155,7 +188,7 @@ class FlowExecutorView(APIView): | |||||||
|                 # we don't show an error message here, but rather call _flow_done() |                 # we don't show an error message here, but rather call _flow_done() | ||||||
|                 return self._flow_done() |                 return self._flow_done() | ||||||
|         # Initial flow request, check if we have an upstream query string passed in |         # Initial flow request, check if we have an upstream query string passed in | ||||||
|         request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", "")) |         request.session[SESSION_KEY_GET] = get_params | ||||||
|         # We don't save the Plan after getting the next stage |         # We don't save the Plan after getting the next stage | ||||||
|         # as it hasn't been successfully passed yet |         # as it hasn't been successfully passed yet | ||||||
|         try: |         try: | ||||||
|  | |||||||
| @ -3,7 +3,9 @@ import os | |||||||
| from collections.abc import Mapping | from collections.abc import Mapping | ||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
| from glob import glob | from glob import glob | ||||||
| from json import dumps | from json import dumps, loads | ||||||
|  | from json.decoder import JSONDecodeError | ||||||
|  | from sys import argv, stderr | ||||||
| from time import time | from time import time | ||||||
| from typing import Any | from typing import Any | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
| @ -59,7 +61,7 @@ class ConfigLoader: | |||||||
|             "timestamp": time(), |             "timestamp": time(), | ||||||
|         } |         } | ||||||
|         output.update(kwargs) |         output.update(kwargs) | ||||||
|         print(dumps(output)) |         print(dumps(output), file=stderr) | ||||||
|  |  | ||||||
|     def update(self, root: dict[str, Any], updatee: dict[str, Any]) -> dict[str, Any]: |     def update(self, root: dict[str, Any], updatee: dict[str, Any]) -> dict[str, Any]: | ||||||
|         """Recursively update dictionary""" |         """Recursively update dictionary""" | ||||||
| @ -81,8 +83,8 @@ class ConfigLoader: | |||||||
|             try: |             try: | ||||||
|                 with open(url.path, "r", encoding="utf8") as _file: |                 with open(url.path, "r", encoding="utf8") as _file: | ||||||
|                     value = _file.read() |                     value = _file.read() | ||||||
|             except OSError: |             except OSError as exc: | ||||||
|                 self._log("error", f"Failed to read config value from {url.path}") |                 self._log("error", f"Failed to read config value from {url.path}: {exc}") | ||||||
|                 value = url.query |                 value = url.query | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
| @ -123,6 +125,11 @@ class ConfigLoader: | |||||||
|                 if dot_part not in current_obj: |                 if dot_part not in current_obj: | ||||||
|                     current_obj[dot_part] = {} |                     current_obj[dot_part] = {} | ||||||
|                 current_obj = current_obj[dot_part] |                 current_obj = current_obj[dot_part] | ||||||
|  |             # Check if the value is json, and try to load it | ||||||
|  |             try: | ||||||
|  |                 value = loads(value) | ||||||
|  |             except JSONDecodeError: | ||||||
|  |                 pass | ||||||
|             current_obj[dot_parts[-1]] = value |             current_obj[dot_parts[-1]] = value | ||||||
|             idx += 1 |             idx += 1 | ||||||
|         if idx > 0: |         if idx > 0: | ||||||
| @ -174,3 +181,9 @@ class ConfigLoader: | |||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG = ConfigLoader() | CONFIG = ConfigLoader() | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     if len(argv) < 2: | ||||||
|  |         print(dumps(CONFIG.raw, indent=4)) | ||||||
|  |     else: | ||||||
|  |         print(CONFIG.y(argv[1])) | ||||||
|  | |||||||
| @ -47,6 +47,7 @@ error_reporting: | |||||||
|   enabled: false |   enabled: false | ||||||
|   environment: customer |   environment: customer | ||||||
|   send_pii: false |   send_pii: false | ||||||
|  |   sample_rate: 0.5 | ||||||
|  |  | ||||||
| # Global email settings | # Global email settings | ||||||
| email: | email: | ||||||
| @ -64,7 +65,7 @@ outposts: | |||||||
|   # %(type)s: Outpost type; proxy, ldap, etc |   # %(type)s: Outpost type; proxy, ldap, etc | ||||||
|   # %(version)s: Current version; 2021.4.1 |   # %(version)s: Current version; 2021.4.1 | ||||||
|   # %(build_hash)s: Build hash if you're running a beta version |   # %(build_hash)s: Build hash if you're running a beta version | ||||||
|   container_image_base: env://AUTHENTIK_OUTPOSTS__DOCKER_IMAGE_BASE?goauthentik.io/%(type)s:%(version)s |   container_image_base: goauthentik.io/%(type)s:%(version)s | ||||||
|  |  | ||||||
| cookie_domain: null | cookie_domain: null | ||||||
| disable_update_check: false | disable_update_check: false | ||||||
| @ -72,9 +73,14 @@ disable_startup_analytics: false | |||||||
| avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar | avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar | ||||||
| geoip: "./GeoLite2-City.mmdb" | geoip: "./GeoLite2-City.mmdb" | ||||||
|  |  | ||||||
| # Can't currently be configured via environment variables, only yaml |  | ||||||
| footer_links: | footer_links: | ||||||
|   - name: Documentation |   - name: Documentation | ||||||
|     href: https://goauthentik.io/docs/?utm_source=authentik |     href: https://goauthentik.io/docs/?utm_source=authentik | ||||||
|   - name: authentik Website |   - name: authentik Website | ||||||
|     href: https://goauthentik.io/?utm_source=authentik |     href: https://goauthentik.io/?utm_source=authentik | ||||||
|  |  | ||||||
|  | default_user_change_email: true | ||||||
|  | default_user_change_username: true | ||||||
|  |  | ||||||
|  | gdpr_compliance: true | ||||||
|  | cert_discovery_dir: /certs | ||||||
|  | |||||||
| @ -66,3 +66,11 @@ class DomainlessURLValidator(URLValidator): | |||||||
|             r"\Z", |             r"\Z", | ||||||
|             re.IGNORECASE, |             re.IGNORECASE, | ||||||
|         ) |         ) | ||||||
|  |         self.schemes = ["http", "https", "blank"] + list(self.schemes) | ||||||
|  |  | ||||||
|  |     def __call__(self, value: str): | ||||||
|  |         # Check if the scheme is valid. | ||||||
|  |         scheme = value.split("://")[0].lower() | ||||||
|  |         if scheme not in self.schemes: | ||||||
|  |             value = "default" + value | ||||||
|  |         super().__call__(value) | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from botocore.exceptions import BotoCoreError | |||||||
| from celery.exceptions import CeleryError | from celery.exceptions import CeleryError | ||||||
| from channels.middleware import BaseMiddleware | from channels.middleware import BaseMiddleware | ||||||
| from channels_redis.core import ChannelFull | from channels_redis.core import ChannelFull | ||||||
|  | from django.conf import settings | ||||||
| from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError | from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError | ||||||
| from django.db import InternalError, OperationalError, ProgrammingError | from django.db import InternalError, OperationalError, ProgrammingError | ||||||
| from django.http.response import Http404 | from django.http.response import Http404 | ||||||
| @ -92,6 +93,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|         # End-user errors |         # End-user errors | ||||||
|         Http404, |         Http404, | ||||||
|     ) |     ) | ||||||
|  |     exc_value = None | ||||||
|     if "exc_info" in hint: |     if "exc_info" in hint: | ||||||
|         _, exc_value, _ = hint["exc_info"] |         _, exc_value, _ = hint["exc_info"] | ||||||
|         if isinstance(exc_value, ignored_classes): |         if isinstance(exc_value, ignored_classes): | ||||||
| @ -105,6 +107,10 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|             "asyncio", |             "asyncio", | ||||||
|             "multiprocessing", |             "multiprocessing", | ||||||
|             "django_redis", |             "django_redis", | ||||||
|  |             "django.security.DisallowedHost", | ||||||
|         ]: |         ]: | ||||||
|             return None |             return None | ||||||
|  |     LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None)) | ||||||
|  |     if settings.DEBUG: | ||||||
|  |         return None | ||||||
|     return event |     return event | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """Test Evaluator base functions""" | """Test Evaluator base functions""" | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.lib.expression.evaluator import BaseEvaluator | from authentik.lib.expression.evaluator import BaseEvaluator | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -19,12 +19,11 @@ class TestEvaluator(TestCase): | |||||||
|  |  | ||||||
|     def test_user_by(self): |     def test_user_by(self): | ||||||
|         """Test expr_user_by""" |         """Test expr_user_by""" | ||||||
|         self.assertIsNotNone(BaseEvaluator.expr_user_by(username="akadmin")) |         user = create_test_admin_user() | ||||||
|  |         self.assertIsNotNone(BaseEvaluator.expr_user_by(username=user.username)) | ||||||
|         self.assertIsNone(BaseEvaluator.expr_user_by(username="bar")) |         self.assertIsNone(BaseEvaluator.expr_user_by(username="bar")) | ||||||
|         self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar")) |         self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar")) | ||||||
|  |  | ||||||
|     def test_is_group_member(self): |     def test_is_group_member(self): | ||||||
|         """Test expr_is_group_member""" |         """Test expr_is_group_member""" | ||||||
|         self.assertFalse( |         self.assertFalse(BaseEvaluator.expr_is_group_member(create_test_admin_user(), name="test")) | ||||||
|             BaseEvaluator.expr_is_group_member(User.objects.get(username="akadmin"), name="test") |  | ||||||
|         ) |  | ||||||
|  | |||||||
| @ -1,17 +1,24 @@ | |||||||
| """Test HTTP Helpers""" | """Test HTTP Helpers""" | ||||||
| from django.test import RequestFactory, TestCase | from django.test import RequestFactory, TestCase | ||||||
|  |  | ||||||
| from authentik.core.models import USER_ATTRIBUTE_CAN_OVERRIDE_IP, Token, TokenIntents, User | from authentik.core.models import USER_ATTRIBUTE_CAN_OVERRIDE_IP, Token, TokenIntents | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.lib.utils.http import OUTPOST_REMOTE_IP_HEADER, OUTPOST_TOKEN_HEADER, get_client_ip | from authentik.lib.utils.http import OUTPOST_REMOTE_IP_HEADER, OUTPOST_TOKEN_HEADER, get_client_ip | ||||||
|  | from authentik.lib.views import bad_request_message | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestHTTP(TestCase): | class TestHTTP(TestCase): | ||||||
|     """Test HTTP Helpers""" |     """Test HTTP Helpers""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.factory = RequestFactory() |         self.factory = RequestFactory() | ||||||
|  |  | ||||||
|  |     def test_bad_request_message(self): | ||||||
|  |         """test bad_request_message""" | ||||||
|  |         request = self.factory.get("/") | ||||||
|  |         self.assertEqual(bad_request_message(request, "foo").status_code, 400) | ||||||
|  |  | ||||||
|     def test_normal(self): |     def test_normal(self): | ||||||
|         """Test normal request""" |         """Test normal request""" | ||||||
|         request = self.factory.get("/") |         request = self.factory.get("/") | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from authentik.managed.manager import ObjectManager | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def managed_reconcile(self: MonitoredTask): | def managed_reconcile(self: MonitoredTask): | ||||||
|     """Run ObjectManager to ensure objects are up-to-date""" |     """Run ObjectManager to ensure objects are up-to-date""" | ||||||
|     try: |     try: | ||||||
| @ -20,5 +20,5 @@ def managed_reconcile(self: MonitoredTask): | |||||||
|         self.set_status( |         self.set_status( | ||||||
|             TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated managed models."]) |             TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated managed models."]) | ||||||
|         ) |         ) | ||||||
|     except DatabaseError as exc: |     except DatabaseError as exc:  # pragma: no cover | ||||||
|         self.set_status(TaskResult(TaskResultStatus.WARNING, [str(exc)])) |         self.set_status(TaskResult(TaskResultStatus.WARNING, [str(exc)])) | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								authentik/managed/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								authentik/managed/tests.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | """managed tests""" | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.managed.tasks import managed_reconcile | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestManaged(TestCase): | ||||||
|  |     """managed tests""" | ||||||
|  |  | ||||||
|  |     def test_reconcile(self): | ||||||
|  |         """Test reconcile""" | ||||||
|  |         # pyright: reportGeneralTypeIssues=false | ||||||
|  |         managed_reconcile()  # pylint: disable=no-value-for-parameter | ||||||
| @ -1,6 +1,8 @@ | |||||||
| """Outpost API Views""" | """Outpost API Views""" | ||||||
| from dacite.core import from_dict | from dacite.core import from_dict | ||||||
| from dacite.exceptions import DaciteError | from dacite.exceptions import DaciteError | ||||||
|  | from django_filters.filters import ModelMultipleChoiceFilter | ||||||
|  | from django_filters.filterset import FilterSet | ||||||
| from drf_spectacular.utils import extend_schema | from drf_spectacular.utils import extend_schema | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import BooleanField, CharField, DateTimeField | from rest_framework.fields import BooleanField, CharField, DateTimeField | ||||||
| @ -99,16 +101,30 @@ class OutpostHealthSerializer(PassiveSerializer): | |||||||
|     version_outdated = BooleanField(read_only=True) |     version_outdated = BooleanField(read_only=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OutpostFilter(FilterSet): | ||||||
|  |     """Filter for Outposts""" | ||||||
|  |  | ||||||
|  |     providers_by_pk = ModelMultipleChoiceFilter( | ||||||
|  |         field_name="providers", | ||||||
|  |         queryset=Provider.objects.all(), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         model = Outpost | ||||||
|  |         fields = { | ||||||
|  |             "providers": ["isnull"], | ||||||
|  |             "name": ["iexact", "icontains"], | ||||||
|  |             "service_connection__name": ["iexact", "icontains"], | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostViewSet(UsedByMixin, ModelViewSet): | class OutpostViewSet(UsedByMixin, ModelViewSet): | ||||||
|     """Outpost Viewset""" |     """Outpost Viewset""" | ||||||
|  |  | ||||||
|     queryset = Outpost.objects.all() |     queryset = Outpost.objects.all() | ||||||
|     serializer_class = OutpostSerializer |     serializer_class = OutpostSerializer | ||||||
|     filterset_fields = { |     filterset_class = OutpostFilter | ||||||
|         "providers": ["isnull"], |  | ||||||
|         "name": ["iexact", "icontains"], |  | ||||||
|         "service_connection__name": ["iexact", "icontains"], |  | ||||||
|     } |  | ||||||
|     search_fields = [ |     search_fields = [ | ||||||
|         "name", |         "name", | ||||||
|         "providers__name", |         "providers__name", | ||||||
|  | |||||||
| @ -46,6 +46,7 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "component", |             "component", | ||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|  |             "meta_model_name", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -19,8 +19,9 @@ class AuthentikOutpostConfig(AppConfig): | |||||||
|         import_module("authentik.outposts.signals") |         import_module("authentik.outposts.signals") | ||||||
|         import_module("authentik.outposts.managed") |         import_module("authentik.outposts.managed") | ||||||
|         try: |         try: | ||||||
|             from authentik.outposts.tasks import outpost_local_connection |             from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection | ||||||
|  |  | ||||||
|             outpost_local_connection.delay() |             outpost_local_connection.delay() | ||||||
|  |             outpost_controller_all.delay() | ||||||
|         except ProgrammingError: |         except ProgrammingError: | ||||||
|             pass |             pass | ||||||
|  | |||||||
| @ -126,7 +126,7 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|         self.send_json(asdict(response)) |         self.send_json(asdict(response)) | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument |     # pylint: disable=unused-argument | ||||||
|     def event_update(self, event): |     def event_update(self, event):  # pragma: no cover | ||||||
|         """Event handler which is called by post_save signals, Send update instruction""" |         """Event handler which is called by post_save signals, Send update instruction""" | ||||||
|         self.send_json( |         self.send_json( | ||||||
|             asdict(WebsocketMessage(instruction=WebsocketMessageInstruction.TRIGGER_UPDATE)) |             asdict(WebsocketMessage(instruction=WebsocketMessageInstruction.TRIGGER_UPDATE)) | ||||||
|  | |||||||
| @ -67,8 +67,6 @@ class OutpostConfig: | |||||||
|     authentik_host_browser: str = "" |     authentik_host_browser: str = "" | ||||||
|  |  | ||||||
|     log_level: str = CONFIG.y("log_level") |     log_level: str = CONFIG.y("log_level") | ||||||
|     error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled") |  | ||||||
|     error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer") |  | ||||||
|     object_naming_template: str = field(default="ak-outpost-%(name)s") |     object_naming_template: str = field(default="ak-outpost-%(name)s") | ||||||
|  |  | ||||||
|     docker_network: Optional[str] = field(default=None) |     docker_network: Optional[str] = field(default=None) | ||||||
|  | |||||||
| @ -76,7 +76,7 @@ def outpost_service_connection_state(connection_pk: Any): | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def outpost_service_connection_monitor(self: MonitoredTask): | def outpost_service_connection_monitor(self: MonitoredTask): | ||||||
|     """Regularly check the state of Outpost Service Connections""" |     """Regularly check the state of Outpost Service Connections""" | ||||||
|     connections = OutpostServiceConnection.objects.all() |     connections = OutpostServiceConnection.objects.all() | ||||||
| @ -126,7 +126,7 @@ def outpost_controller( | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def outpost_token_ensurer(self: MonitoredTask): | def outpost_token_ensurer(self: MonitoredTask): | ||||||
|     """Periodically ensure that all Outposts have valid Service Accounts |     """Periodically ensure that all Outposts have valid Service Accounts | ||||||
|     and Tokens""" |     and Tokens""" | ||||||
|  | |||||||
| @ -2,8 +2,8 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import PropertyMapping, User | from authentik.core.models import PropertyMapping | ||||||
| from authentik.flows.models import Flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.outposts.api.outposts import OutpostSerializer | from authentik.outposts.api.outposts import OutpostSerializer | ||||||
| from authentik.outposts.models import OutpostType, default_outpost_config | from authentik.outposts.models import OutpostType, default_outpost_config | ||||||
| from authentik.providers.ldap.models import LDAPProvider | from authentik.providers.ldap.models import LDAPProvider | ||||||
| @ -18,7 +18,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase): | |||||||
|         self.mapping = PropertyMapping.objects.create( |         self.mapping = PropertyMapping.objects.create( | ||||||
|             name="dummy", expression="""return {'foo': 'bar'}""" |             name="dummy", expression="""return {'foo': 'bar'}""" | ||||||
|         ) |         ) | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_outpost_validaton(self): |     def test_outpost_validaton(self): | ||||||
| @ -30,7 +30,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase): | |||||||
|                 "config": default_outpost_config(), |                 "config": default_outpost_config(), | ||||||
|                 "providers": [ |                 "providers": [ | ||||||
|                     ProxyProvider.objects.create( |                     ProxyProvider.objects.create( | ||||||
|                         name="test", authorization_flow=Flow.objects.first() |                         name="test", authorization_flow=create_test_flow() | ||||||
|                     ).pk |                     ).pk | ||||||
|                 ], |                 ], | ||||||
|             } |             } | ||||||
| @ -43,7 +43,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase): | |||||||
|                 "config": default_outpost_config(), |                 "config": default_outpost_config(), | ||||||
|                 "providers": [ |                 "providers": [ | ||||||
|                     LDAPProvider.objects.create( |                     LDAPProvider.objects.create( | ||||||
|                         name="test", authorization_flow=Flow.objects.first() |                         name="test", authorization_flow=create_test_flow() | ||||||
|                     ).pk |                     ).pk | ||||||
|                 ], |                 ], | ||||||
|             } |             } | ||||||
| @ -60,9 +60,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def test_outpost_config(self): |     def test_outpost_config(self): | ||||||
|         """Test Outpost's config field""" |         """Test Outpost's config field""" | ||||||
|         provider = ProxyProvider.objects.create( |         provider = ProxyProvider.objects.create(name="test", authorization_flow=create_test_flow()) | ||||||
|             name="test", authorization_flow=Flow.objects.first() |  | ||||||
|         ) |  | ||||||
|         invalid = OutpostSerializer(data={"name": "foo", "providers": [provider.pk], "config": ""}) |         invalid = OutpostSerializer(data={"name": "foo", "providers": [provider.pk], "config": ""}) | ||||||
|         self.assertFalse(invalid.is_valid()) |         self.assertFalse(invalid.is_valid()) | ||||||
|         self.assertIn("config", invalid.errors) |         self.assertIn("config", invalid.errors) | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								authentik/outposts/tests/test_commands.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								authentik/outposts/tests/test_commands.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | """management command tests""" | ||||||
|  | from io import StringIO | ||||||
|  |  | ||||||
|  | from django.core.management import call_command | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestManagementCommands(TestCase): | ||||||
|  |     """management command tests""" | ||||||
|  |  | ||||||
|  |     def test_repair_permissions(self): | ||||||
|  |         """Test repair_permissions""" | ||||||
|  |         out = StringIO() | ||||||
|  |         call_command("repair_permissions", stdout=out) | ||||||
|  |         self.assertNotEqual(out.getvalue(), "") | ||||||
| @ -4,8 +4,7 @@ from django.contrib.auth.management import create_permissions | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from guardian.models import UserObjectPermission | from guardian.models import UserObjectPermission | ||||||
|  |  | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.core.tests.utils import create_test_cert, create_test_flow | ||||||
| from authentik.flows.models import Flow |  | ||||||
| from authentik.outposts.models import Outpost, OutpostType | from authentik.outposts.models import Outpost, OutpostType | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
|  |  | ||||||
| @ -23,7 +22,7 @@ class OutpostTests(TestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             internal_host="http://localhost", |             internal_host="http://localhost", | ||||||
|             external_host="http://localhost", |             external_host="http://localhost", | ||||||
|             authorization_flow=Flow.objects.first(), |             authorization_flow=create_test_flow(), | ||||||
|         ) |         ) | ||||||
|         outpost: Outpost = Outpost.objects.create( |         outpost: Outpost = Outpost.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
| @ -45,7 +44,7 @@ class OutpostTests(TestCase): | |||||||
|         self.assertEqual(permissions[1].object_pk, str(provider.pk)) |         self.assertEqual(permissions[1].object_pk, str(provider.pk)) | ||||||
|  |  | ||||||
|         # Provider requires a certificate-key-pair, user should have permissions for it |         # Provider requires a certificate-key-pair, user should have permissions for it | ||||||
|         keypair = CertificateKeyPair.objects.first() |         keypair = create_test_cert() | ||||||
|         provider.certificate = keypair |         provider.certificate = keypair | ||||||
|         provider.save() |         provider.save() | ||||||
|         permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by( |         permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by( | ||||||
|  | |||||||
							
								
								
									
										97
									
								
								authentik/outposts/tests/test_ws.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								authentik/outposts/tests/test_ws.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | """Websocket tests""" | ||||||
|  | from dataclasses import asdict | ||||||
|  |  | ||||||
|  | from channels.routing import URLRouter | ||||||
|  | from channels.testing import WebsocketCommunicator | ||||||
|  | from django.test import TransactionTestCase | ||||||
|  |  | ||||||
|  | from authentik import __version__ | ||||||
|  | from authentik.flows.models import Flow, FlowDesignation | ||||||
|  | from authentik.outposts.channels import WebsocketMessage, WebsocketMessageInstruction | ||||||
|  | from authentik.outposts.models import Outpost, OutpostType | ||||||
|  | from authentik.providers.proxy.models import ProxyProvider | ||||||
|  | from authentik.root import websocket | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestOutpostWS(TransactionTestCase): | ||||||
|  |     """Websocket tests""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         self.provider: ProxyProvider = ProxyProvider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             internal_host="http://localhost", | ||||||
|  |             external_host="http://localhost", | ||||||
|  |             authorization_flow=Flow.objects.create( | ||||||
|  |                 name="foo", slug="foo", designation=FlowDesignation.AUTHORIZATION | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         self.outpost: Outpost = Outpost.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             type=OutpostType.PROXY, | ||||||
|  |         ) | ||||||
|  |         self.outpost.providers.add(self.provider) | ||||||
|  |         self.token = self.outpost.token.key | ||||||
|  |  | ||||||
|  |     async def test_auth(self): | ||||||
|  |         """Test auth without token""" | ||||||
|  |         communicator = WebsocketCommunicator( | ||||||
|  |             URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/" | ||||||
|  |         ) | ||||||
|  |         connected, _ = await communicator.connect() | ||||||
|  |         self.assertFalse(connected) | ||||||
|  |  | ||||||
|  |     async def test_auth_valid(self): | ||||||
|  |         """Test auth with token""" | ||||||
|  |         communicator = WebsocketCommunicator( | ||||||
|  |             URLRouter(websocket.websocket_urlpatterns), | ||||||
|  |             f"/ws/outpost/{self.outpost.pk}/", | ||||||
|  |             {b"authorization": f"Bearer {self.token}".encode()}, | ||||||
|  |         ) | ||||||
|  |         connected, _ = await communicator.connect() | ||||||
|  |         self.assertTrue(connected) | ||||||
|  |  | ||||||
|  |     async def test_send(self): | ||||||
|  |         """Test sending of Hello""" | ||||||
|  |         communicator = WebsocketCommunicator( | ||||||
|  |             URLRouter(websocket.websocket_urlpatterns), | ||||||
|  |             f"/ws/outpost/{self.outpost.pk}/", | ||||||
|  |             {b"authorization": f"Bearer {self.token}".encode()}, | ||||||
|  |         ) | ||||||
|  |         connected, _ = await communicator.connect() | ||||||
|  |         self.assertTrue(connected) | ||||||
|  |         await communicator.send_json_to( | ||||||
|  |             asdict( | ||||||
|  |                 WebsocketMessage( | ||||||
|  |                     instruction=WebsocketMessageInstruction.HELLO, | ||||||
|  |                     args={ | ||||||
|  |                         "version": __version__, | ||||||
|  |                         "buildHash": "foo", | ||||||
|  |                         "uuid": "123", | ||||||
|  |                     }, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         response = await communicator.receive_json_from() | ||||||
|  |         self.assertEqual( | ||||||
|  |             response, asdict(WebsocketMessage(instruction=WebsocketMessageInstruction.ACK, args={})) | ||||||
|  |         ) | ||||||
|  |         await communicator.disconnect() | ||||||
|  |  | ||||||
|  |     async def test_send_ack(self): | ||||||
|  |         """Test sending of ACK""" | ||||||
|  |         communicator = WebsocketCommunicator( | ||||||
|  |             URLRouter(websocket.websocket_urlpatterns), | ||||||
|  |             f"/ws/outpost/{self.outpost.pk}/", | ||||||
|  |             {b"authorization": f"Bearer {self.token}".encode()}, | ||||||
|  |         ) | ||||||
|  |         connected, _ = await communicator.connect() | ||||||
|  |         self.assertTrue(connected) | ||||||
|  |         await communicator.send_json_to( | ||||||
|  |             asdict( | ||||||
|  |                 WebsocketMessage( | ||||||
|  |                     instruction=WebsocketMessageInstruction.ACK, | ||||||
|  |                     args={}, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         await communicator.disconnect() | ||||||
| @ -66,6 +66,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "component", |             "component", | ||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|  |             "meta_model_name", | ||||||
|             "bound_to", |             "bound_to", | ||||||
|         ] |         ] | ||||||
|         depth = 3 |         depth = 3 | ||||||
|  | |||||||
| @ -69,8 +69,8 @@ class Migration(migrations.Migration): | |||||||
|                     ("authentik.stages.user_logout", "authentik Stages.User Logout"), |                     ("authentik.stages.user_logout", "authentik Stages.User Logout"), | ||||||
|                     ("authentik.stages.user_write", "authentik Stages.User Write"), |                     ("authentik.stages.user_write", "authentik Stages.User Write"), | ||||||
|                     ("authentik.tenants", "authentik Tenants"), |                     ("authentik.tenants", "authentik Tenants"), | ||||||
|                     ("authentik.core", "authentik Core"), |  | ||||||
|                     ("authentik.managed", "authentik Managed"), |                     ("authentik.managed", "authentik Managed"), | ||||||
|  |                     ("authentik.core", "authentik Core"), | ||||||
|                 ], |                 ], | ||||||
|                 default="", |                 default="", | ||||||
|                 help_text="Match events created by selected application. When left empty, all applications are matched.", |                 help_text="Match events created by selected application. When left empty, all applications are matched.", | ||||||
|  | |||||||
| @ -11,6 +11,8 @@ from authentik.flows.planner import PLAN_CONTEXT_SSO | |||||||
| from authentik.lib.expression.evaluator import BaseEvaluator | from authentik.lib.expression.evaluator import BaseEvaluator | ||||||
| from authentik.lib.utils.http import get_client_ip | from authentik.lib.utils.http import get_client_ip | ||||||
| from authentik.policies.exceptions import PolicyException | from authentik.policies.exceptions import PolicyException | ||||||
|  | from authentik.policies.models import Policy, PolicyBinding | ||||||
|  | from authentik.policies.process import PolicyProcess | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| @ -31,6 +33,7 @@ class PolicyEvaluator(BaseEvaluator): | |||||||
|         self._context["ak_logger"] = get_logger(policy_name) |         self._context["ak_logger"] = get_logger(policy_name) | ||||||
|         self._context["ak_message"] = self.expr_func_message |         self._context["ak_message"] = self.expr_func_message | ||||||
|         self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator |         self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator | ||||||
|  |         self._context["ak_call_policy"] = self.expr_func_call_policy | ||||||
|         self._context["ip_address"] = ip_address |         self._context["ip_address"] = ip_address | ||||||
|         self._context["ip_network"] = ip_network |         self._context["ip_network"] = ip_network | ||||||
|         self._filename = policy_name or "PolicyEvaluator" |         self._filename = policy_name or "PolicyEvaluator" | ||||||
| @ -39,6 +42,16 @@ class PolicyEvaluator(BaseEvaluator): | |||||||
|         """Wrapper to append to messages list, which is returned with PolicyResult""" |         """Wrapper to append to messages list, which is returned with PolicyResult""" | ||||||
|         self._messages.append(message) |         self._messages.append(message) | ||||||
|  |  | ||||||
|  |     def expr_func_call_policy(self, name: str, **kwargs) -> PolicyResult: | ||||||
|  |         """Call policy by name, with current request""" | ||||||
|  |         policy = Policy.objects.filter(name=name).select_subclasses().first() | ||||||
|  |         if not policy: | ||||||
|  |             raise ValueError(f"Policy '{name}' not found.") | ||||||
|  |         req: PolicyRequest = self._context["request"] | ||||||
|  |         req.context.update(kwargs) | ||||||
|  |         proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None) | ||||||
|  |         return proc.profiling_wrapper() | ||||||
|  |  | ||||||
|     def expr_func_user_has_authenticator( |     def expr_func_user_has_authenticator( | ||||||
|         self, user: User, device_type: Optional[str] = None |         self, user: User, device_type: Optional[str] = None | ||||||
|     ) -> bool: |     ) -> bool: | ||||||
| @ -50,7 +63,7 @@ class PolicyEvaluator(BaseEvaluator): | |||||||
|                 if device_class == device_type: |                 if device_class == device_type: | ||||||
|                     return True |                     return True | ||||||
|             return False |             return False | ||||||
|         return len(user_devices) > 0 |         return len(list(user_devices)) > 0 | ||||||
|  |  | ||||||
|     def set_policy_request(self, request: PolicyRequest): |     def set_policy_request(self, request: PolicyRequest): | ||||||
|         """Update context based on policy request (if http request is given, update that too)""" |         """Update context based on policy request (if http request is given, update that too)""" | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ class TestHIBPPolicy(TestCase): | |||||||
|             name="test_false", |             name="test_false", | ||||||
|         ) |         ) | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["password"] = "password" |         request.context["password"] = "password"  # nosec | ||||||
|         result: PolicyResult = policy.passes(request) |         result: PolicyResult = policy.passes(request) | ||||||
|         self.assertFalse(result.passing) |         self.assertFalse(result.passing) | ||||||
|         self.assertTrue(result.messages[0].startswith("Password exists on ")) |         self.assertTrue(result.messages[0].startswith("Password exists on ")) | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ class TestPasswordPolicy(TestCase): | |||||||
|     def test_failed_length(self): |     def test_failed_length(self): | ||||||
|         """Password too short""" |         """Password too short""" | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["password"] = "test" |         request.context["password"] = "test"  # nosec | ||||||
|         result: PolicyResult = self.policy.passes(request) |         result: PolicyResult = self.policy.passes(request) | ||||||
|         self.assertFalse(result.passing) |         self.assertFalse(result.passing) | ||||||
|         self.assertEqual(result.messages, ("test message",)) |         self.assertEqual(result.messages, ("test message",)) | ||||||
| @ -38,7 +38,7 @@ class TestPasswordPolicy(TestCase): | |||||||
|     def test_failed_lowercase(self): |     def test_failed_lowercase(self): | ||||||
|         """not enough lowercase""" |         """not enough lowercase""" | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe" |         request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe"  # nosec | ||||||
|         result: PolicyResult = self.policy.passes(request) |         result: PolicyResult = self.policy.passes(request) | ||||||
|         self.assertFalse(result.passing) |         self.assertFalse(result.passing) | ||||||
|         self.assertEqual(result.messages, ("test message",)) |         self.assertEqual(result.messages, ("test message",)) | ||||||
| @ -46,7 +46,7 @@ class TestPasswordPolicy(TestCase): | |||||||
|     def test_failed_uppercase(self): |     def test_failed_uppercase(self): | ||||||
|         """not enough uppercase""" |         """not enough uppercase""" | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["password"] = "tttttttttttttttttttttttE" |         request.context["password"] = "tttttttttttttttttttttttE"  # nosec | ||||||
|         result: PolicyResult = self.policy.passes(request) |         result: PolicyResult = self.policy.passes(request) | ||||||
|         self.assertFalse(result.passing) |         self.assertFalse(result.passing) | ||||||
|         self.assertEqual(result.messages, ("test message",)) |         self.assertEqual(result.messages, ("test message",)) | ||||||
| @ -54,7 +54,7 @@ class TestPasswordPolicy(TestCase): | |||||||
|     def test_failed_symbols(self): |     def test_failed_symbols(self): | ||||||
|         """not enough uppercase""" |         """not enough uppercase""" | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["password"] = "TETETETETETETETETETETETETe!!!" |         request.context["password"] = "TETETETETETETETETETETETETe!!!"  # nosec | ||||||
|         result: PolicyResult = self.policy.passes(request) |         result: PolicyResult = self.policy.passes(request) | ||||||
|         self.assertFalse(result.passing) |         self.assertFalse(result.passing) | ||||||
|         self.assertEqual(result.messages, ("test message",)) |         self.assertEqual(result.messages, ("test message",)) | ||||||
| @ -62,7 +62,7 @@ class TestPasswordPolicy(TestCase): | |||||||
|     def test_true(self): |     def test_true(self): | ||||||
|         """Positive password case""" |         """Positive password case""" | ||||||
|         request = PolicyRequest(get_anonymous_user()) |         request = PolicyRequest(get_anonymous_user()) | ||||||
|         request.context["password"] = generate_key() + "ee!!!" |         request.context["password"] = generate_key() + "ee!!!"  # nosec | ||||||
|         result: PolicyResult = self.policy.passes(request) |         result: PolicyResult = self.policy.passes(request) | ||||||
|         self.assertTrue(result.passing) |         self.assertTrue(result.passing) | ||||||
|         self.assertEqual(result.messages, tuple()) |         self.assertEqual(result.messages, tuple()) | ||||||
|  | |||||||
| @ -127,8 +127,8 @@ class PolicyProcess(PROCESS_CLASS): | |||||||
|         ) |         ) | ||||||
|         return policy_result |         return policy_result | ||||||
|  |  | ||||||
|     def run(self):  # pragma: no cover |     def profiling_wrapper(self): | ||||||
|         """Task wrapper to run policy checking""" |         """Run with profiling enabled""" | ||||||
|         with Hub.current.start_span( |         with Hub.current.start_span( | ||||||
|             op="policy.process.execute", |             op="policy.process.execute", | ||||||
|         ) as span, HIST_POLICIES_EXECUTION_TIME.labels( |         ) as span, HIST_POLICIES_EXECUTION_TIME.labels( | ||||||
| @ -142,8 +142,12 @@ class PolicyProcess(PROCESS_CLASS): | |||||||
|             span: Span |             span: Span | ||||||
|             span.set_data("policy", self.binding.policy) |             span.set_data("policy", self.binding.policy) | ||||||
|             span.set_data("request", self.request) |             span.set_data("request", self.request) | ||||||
|  |             return self.execute() | ||||||
|  |  | ||||||
|  |     def run(self):  # pragma: no cover | ||||||
|  |         """Task wrapper to run policy checking""" | ||||||
|         try: |         try: | ||||||
|                 self.connection.send(self.execute()) |             self.connection.send(self.profiling_wrapper()) | ||||||
|         except Exception as exc:  # pylint: disable=broad-except |         except Exception as exc:  # pylint: disable=broad-except | ||||||
|             LOGGER.warning(str(exc)) |             LOGGER.warning(str(exc)) | ||||||
|             self.connection.send(PolicyResult(False, str(exc))) |             self.connection.send(PolicyResult(False, str(exc))) | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ LOGGER = get_logger() | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def save_ip_reputation(self: MonitoredTask): | def save_ip_reputation(self: MonitoredTask): | ||||||
|     """Save currently cached reputation to database""" |     """Save currently cached reputation to database""" | ||||||
|     objects_to_update = [] |     objects_to_update = [] | ||||||
| @ -30,7 +30,7 @@ def save_ip_reputation(self: MonitoredTask): | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @prefill_task() | @prefill_task | ||||||
| def save_user_reputation(self: MonitoredTask): | def save_user_reputation(self: MonitoredTask): | ||||||
|     """Save currently cached reputation to database""" |     """Save currently cached reputation to database""" | ||||||
|     objects_to_update = [] |     objects_to_update = [] | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import Group, User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.policies.models import PolicyBindingModel | from authentik.policies.models import PolicyBindingModel | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -12,8 +12,8 @@ class TestBindingsAPI(APITestCase): | |||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.pbm = PolicyBindingModel.objects.create() |         self.pbm = PolicyBindingModel.objects.create() | ||||||
|         self.group = Group.objects.first() |         self.user = create_test_admin_user() | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.group = self.user.ak_groups.first() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_valid_binding(self): |     def test_valid_binding(self): | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.policies.dummy.models import DummyPolicy | from authentik.policies.dummy.models import DummyPolicy | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -12,7 +12,7 @@ class TestPoliciesAPI(APITestCase): | |||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.policy = DummyPolicy.objects.create(name="dummy", result=True) |         self.policy = DummyPolicy.objects.create(name="dummy", result=True) | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_test_call(self): |     def test_test_call(self): | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ from django.views.generic.base import View | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Application, Provider, User | from authentik.core.models import Application, Provider, User | ||||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE | from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.policies.denied import AccessDeniedResponse | from authentik.policies.denied import AccessDeniedResponse | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| @ -84,6 +84,10 @@ class PolicyAccessView(AccessMixin, View): | |||||||
|         a hint on the Identification Stage what the user should login for.""" |         a hint on the Identification Stage what the user should login for.""" | ||||||
|         if self.application: |         if self.application: | ||||||
|             self.request.session[SESSION_KEY_APPLICATION_PRE] = self.application |             self.request.session[SESSION_KEY_APPLICATION_PRE] = self.application | ||||||
|  |         # Because this view might get hit with a POST request, we need to preserve that data | ||||||
|  |         # since later views might need it (mostly SAML) | ||||||
|  |         if self.request.method.lower() == "post": | ||||||
|  |             self.request.session[SESSION_KEY_POST] = self.request.POST | ||||||
|         return redirect_to_login( |         return redirect_to_login( | ||||||
|             self.request.get_full_path(), |             self.request.get_full_path(), | ||||||
|             self.get_login_url(), |             self.get_login_url(), | ||||||
|  | |||||||
| @ -2,8 +2,7 @@ | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.flows.models import Flow, FlowDesignation |  | ||||||
| from authentik.providers.oauth2.models import JWTAlgorithms | from authentik.providers.oauth2.models import JWTAlgorithms | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -12,7 +11,7 @@ class TestOAuth2ProviderAPI(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.get(username="akadmin") |         self.user = create_test_admin_user() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_validate(self): |     def test_validate(self): | ||||||
| @ -24,9 +23,7 @@ class TestOAuth2ProviderAPI(APITestCase): | |||||||
|             data={ |             data={ | ||||||
|                 "name": "test", |                 "name": "test", | ||||||
|                 "jwt_alg": str(JWTAlgorithms.RS256), |                 "jwt_alg": str(JWTAlgorithms.RS256), | ||||||
|                 "authorization_flow": Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION) |                 "authorization_flow": create_test_flow().pk, | ||||||
|                 .first() |  | ||||||
|                 .pk, |  | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         self.assertJSONEqual( |         self.assertJSONEqual( | ||||||
|  | |||||||
| @ -3,8 +3,8 @@ from django.test import RequestFactory | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  |  | ||||||
| from authentik.core.models import Application, User | from authentik.core.models import Application | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| @ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         OAuth2Provider.objects.create( |         OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=Flow.objects.first(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(AuthorizeError): |         with self.assertRaises(AuthorizeError): | ||||||
| @ -63,7 +63,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         OAuth2Provider.objects.create( |         OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=Flow.objects.first(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
| @ -85,7 +85,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         OAuth2Provider.objects.create( |         OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=Flow.objects.first(), |             authorization_flow=create_test_flow(), | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(RedirectUriError): |         with self.assertRaises(RedirectUriError): | ||||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) |             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||||
| @ -105,7 +105,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         OAuth2Provider.objects.create( |         OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=Flow.objects.first(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|         ) |         ) | ||||||
|         request = self.factory.get( |         request = self.factory.get( | ||||||
| @ -184,7 +184,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|         state = generate_id() |         state = generate_id() | ||||||
|         user = User.objects.get(username="akadmin") |         user = create_test_admin_user() | ||||||
|         self.client.force_login(user) |         self.client.force_login(user) | ||||||
|         # Step 1, initiate params and get redirect to flow |         # Step 1, initiate params and get redirect to flow | ||||||
|         self.client.get( |         self.client.get( | ||||||
| @ -218,11 +218,11 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             client_secret=generate_key(), |             client_secret=generate_key(), | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris="http://localhost", |             redirect_uris="http://localhost", | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |             rsa_key=create_test_cert(), | ||||||
|         ) |         ) | ||||||
|         Application.objects.create(name="app", slug="app", provider=provider) |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|         state = generate_id() |         state = generate_id() | ||||||
|         user = User.objects.get(username="akadmin") |         user = create_test_admin_user() | ||||||
|         self.client.force_login(user) |         self.client.force_login(user) | ||||||
|         # Step 1, initiate params and get redirect to flow |         # Step 1, initiate params and get redirect to flow | ||||||
|         self.client.get( |         self.client.get( | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	