Compare commits
	
		
			253 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e1bae1240f | |||
| 37bd62d291 | |||
| ac63db0136 | |||
| 5cdf3a09a9 | |||
| 3e17adf33f | |||
| 8392916c84 | |||
| 7e75a48fd0 | |||
| d69d84e48c | |||
| 78cc8fa498 | |||
| 0fcdf5e968 | |||
| f05997740f | |||
| 1aff300171 | |||
| ffb98eaa75 | |||
| 5c1db432f0 | |||
| 07fd4daa3e | |||
| 9faad8a055 | |||
| a94392808f | |||
| c4998e7dd4 | |||
| 1ab587d80e | |||
| 5715ffd845 | |||
| 8c3834e6b2 | |||
| f841586153 | |||
| b8b681250f | |||
| 3ab9ee5acc | |||
| 1a4c640835 | |||
| 38bf0ee740 | |||
| 520fb2fac1 | |||
| 95adc38ff4 | |||
| 55ad2d7eab | |||
| 8160663214 | |||
| aa80babfff | |||
| 6a700cb376 | |||
| e123afd9ee | |||
| 96e732e45b | |||
| 6349ab60e7 | |||
| 2b0749af6b | |||
| a5098364eb | |||
| 71820191a3 | |||
| c08c849fec | |||
| 6a74fa11c6 | |||
| 7841720acf | |||
| 67644ace87 | |||
| f84a10b59b | |||
| 200d6d6adf | |||
| d0f1ebfad3 | |||
| 7d849d7bd7 | |||
| f1dfe04786 | |||
| 4d7d2b8d3a | |||
| a6cc0f189c | |||
| 18a4eac527 | |||
| 6dd2e2b85f | |||
| 7bfea87864 | |||
| 1ca8feb5fc | |||
| c1615d044b | |||
| edc9d60e22 | |||
| e6b135d535 | |||
| 8cfad9a854 | |||
| 2237358633 | |||
| d15cd9ce5f | |||
| 62abe22673 | |||
| 8b78570597 | |||
| 549e4dcb94 | |||
| 1480ff6732 | |||
| 0e1000764d | |||
| 8dc9b43bb5 | |||
| 3ce0aa54c7 | |||
| b5888e79f5 | |||
| 25d779e879 | |||
| d1fbb85821 | |||
| ea307689d4 | |||
| 7a06c1685b | |||
| 977757f561 | |||
| c117d98e27 | |||
| 711e98d049 | |||
| f84c176bd0 | |||
| c4b11ca861 | |||
| 132a353b92 | |||
| bb464aad50 | |||
| ab27cd0a9a | |||
| 241280f2b5 | |||
| d110b5b661 | |||
| 8871a4acb2 | |||
| a1ad357abd | |||
| 81f9842797 | |||
| 712256cdfe | |||
| fb4808418c | |||
| 7c7bb9dc2e | |||
| 9a3809135e | |||
| de13265997 | |||
| 0228ea9a4c | |||
| faf986c231 | |||
| 315eae009f | |||
| 02f75a92ce | |||
| a92786e153 | |||
| 157c23946e | |||
| f6b33d65af | |||
| ce461631b5 | |||
| 2f106a9049 | |||
| 7038431e19 | |||
| 3fd9b53fe6 | |||
| e542783fec | |||
| adcd11b1f8 | |||
| 6192d01b7e | |||
| fd2677af1f | |||
| 5947c7b97e | |||
| 986d7bf714 | |||
| 6282e923d6 | |||
| 88b4125a6a | |||
| 208c2d1913 | |||
| 54dc0a46b4 | |||
| fc807744bf | |||
| 9666d407b4 | |||
| 75510ead84 | |||
| 73bf6fd530 | |||
| 2e5a33f0c2 | |||
| 8c33d13dff | |||
| a70de69228 | |||
| ab2d39dd2a | |||
| 2084156f1d | |||
| 1d2725825c | |||
| b9754f9c13 | |||
| bb2e5b4861 | |||
| 89abc99dc0 | |||
| f92c661d09 | |||
| 3468afc399 | |||
| a286ae276b | |||
| 4fdd978b57 | |||
| c52bd8c4b9 | |||
| ca5ae5f914 | |||
| 4604c92046 | |||
| 4218ece2a5 | |||
| 0d6481c4d5 | |||
| a7fc579202 | |||
| 5600261852 | |||
| 824737965d | |||
| 5476f517da | |||
| d38043fe72 | |||
| 102570c61a | |||
| 238e6e3f24 | |||
| 89c7e61769 | |||
| b097cf4d7e | |||
| 5c0d7f9a58 | |||
| 95b99e3e55 | |||
| 6437fbc814 | |||
| d6fa19a97f | |||
| 1957717160 | |||
| 94a93adb4b | |||
| 5d84f2a079 | |||
| 5b9f35a4a1 | |||
| b3dd87bbab | |||
| af7189953c | |||
| 35d2e9cd5f | |||
| 9a52d8db83 | |||
| 14f0034a0a | |||
| 20522558fe | |||
| f00ee5c174 | |||
| 95e24c9ec2 | |||
| 6b42e404bf | |||
| 9abd4b3e14 | |||
| 865138e7e7 | |||
| 7524413b22 | |||
| 70bdbfd5ef | |||
| 73a7c0c559 | |||
| cafff808ab | |||
| bbbbc2a718 | |||
| 1452f2680a | |||
| dd39aab1fb | |||
| 524fbd5838 | |||
| bb7c3456fa | |||
| b611fd10a2 | |||
| 65b1cbc010 | |||
| 119f64159b | |||
| 1352ed7e44 | |||
| 34ce85fcd1 | |||
| 977ae4f225 | |||
| a464ffe846 | |||
| 6757d43d33 | |||
| da3222df07 | |||
| 54cacd784c | |||
| 32840d3909 | |||
| eb78632853 | |||
| 4868d4a14d | |||
| 3f5effb1bc | |||
| 84c2da8a6e | |||
| 56744659e4 | |||
| bad7deb52a | |||
| 5748e19845 | |||
| 16a03160d0 | |||
| a566856b65 | |||
| 8b52d711e8 | |||
| 4da18b5f0c | |||
| 63e3f6545b | |||
| e35c3d19bc | |||
| ef028af7d1 | |||
| b69c26d485 | |||
| e13cfec84f | |||
| 97df7848a5 | |||
| e2d3a95c80 | |||
| bebf18f257 | |||
| 53e68b8540 | |||
| 9dbd54690c | |||
| 9e41b7d208 | |||
| 1c66d420c4 | |||
| 0ca913f8d4 | |||
| b97274058c | |||
| aef0333695 | |||
| c847b16b3e | |||
| e2e83f5631 | |||
| 8363016982 | |||
| 397b9845ec | |||
| b9da24c952 | |||
| 1053962bec | |||
| 19ff8129e5 | |||
| 40cdf6877d | |||
| 2a399cf8e8 | |||
| 345fa1bed6 | |||
| 70ffb6d49e | |||
| 3ecdcebd35 | |||
| 4f02c8ab98 | |||
| 41974c3f82 | |||
| 808f697423 | |||
| a9dc3ff0d8 | |||
| acde584cbd | |||
| df52116135 | |||
| eaf56f4f3f | |||
| fd9293e3e8 | |||
| 520de8d5b0 | |||
| bbdb0df42e | |||
| 9310d4cdc0 | |||
| 86f9056d3f | |||
| 5375637eda | |||
| 109f06c3ae | |||
| a3744da3a5 | |||
| ff1feb653b | |||
| 4a11d89a08 | |||
| 73d7b5f110 | |||
| 8b7a92068b | |||
| ff1532da13 | |||
| 6eafa2346d | |||
| 681644b854 | |||
| de4d388e0a | |||
| cbe2cb51e7 | |||
| 9176c71075 | |||
| 1c05e4ca09 | |||
| 2d55d3c743 | |||
| 0a9482b28a | |||
| 4b1440944e | |||
| 75794defc6 | |||
| 59a92dbacd | |||
| b81ddf2b80 | |||
| 9ccd1ce08b | |||
| 6f6d22da13 | |||
| 095850f038 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2023.3.0 | current_version = 2023.4.2 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) | ||||||
|  | |||||||
| @ -7,8 +7,14 @@ charset = utf-8 | |||||||
| trim_trailing_whitespace = true | trim_trailing_whitespace = true | ||||||
| insert_final_newline = true | insert_final_newline = true | ||||||
|  |  | ||||||
| [html] | [*.html] | ||||||
| indent_size = 2 | indent_size = 2 | ||||||
|  |  | ||||||
| [yaml] | [*.{yaml,yml}] | ||||||
| indent_size = 2 | indent_size = 2 | ||||||
|  |  | ||||||
|  | [*.go] | ||||||
|  | indent_style = tab | ||||||
|  |  | ||||||
|  | [Makefile] | ||||||
|  | indent_style = tab | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,11 @@ | |||||||
| name: 'Setup authentik testing environment' | name: 'Setup authentik testing environment' | ||||||
| description: 'Setup authentik testing environment' | description: 'Setup authentik testing environment' | ||||||
|  |  | ||||||
|  | inputs: | ||||||
|  |   postgresql_tag: | ||||||
|  |     description: "Optional postgresql image tag" | ||||||
|  |     default: "12" | ||||||
|  |  | ||||||
| runs: | runs: | ||||||
|   using: "composite" |   using: "composite" | ||||||
|   steps: |   steps: | ||||||
| @ -24,6 +29,7 @@ runs: | |||||||
|     - name: Setup dependencies |     - name: Setup dependencies | ||||||
|       shell: bash |       shell: bash | ||||||
|       run: | |       run: | | ||||||
|  |         export PSQL_TAG=${{ inputs.postgresql_tag }} | ||||||
|         docker-compose -f .github/actions/setup/docker-compose.yml up -d |         docker-compose -f .github/actions/setup/docker-compose.yml up -d | ||||||
|         poetry env use python3.11 |         poetry env use python3.11 | ||||||
|         poetry install |         poetry install | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/actions/setup/docker-compose.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/setup/docker-compose.yml
									
									
									
									
										vendored
									
									
								
							| @ -3,7 +3,7 @@ version: '3.7' | |||||||
| services: | services: | ||||||
|   postgresql: |   postgresql: | ||||||
|     container_name: postgres |     container_name: postgres | ||||||
|     image: library/postgres:12 |     image: library/postgres:${PSQL_TAG:-12} | ||||||
|     volumes: |     volumes: | ||||||
|     - db-data:/var/lib/postgresql/data |     - db-data:/var/lib/postgresql/data | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								.github/codecov.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/codecov.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,10 @@ | |||||||
| coverage: | coverage: | ||||||
|   precision: 2 |   status: | ||||||
|   round: up |     project: | ||||||
|  |       default: | ||||||
|  |         target: auto | ||||||
|  |         # adjust accordingly based on how flaky your tests are | ||||||
|  |         # this allows a 1% drop from the previous base commit coverage | ||||||
|  |         threshold: 1% | ||||||
|  |   notify: | ||||||
|  |     after_n_builds: 3 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.github/codespell-dictionary.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.github/codespell-dictionary.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | authentic->authentik | ||||||
							
								
								
									
										1
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							| @ -16,3 +16,4 @@ markComment: > | |||||||
|   This issue has been automatically marked as stale because it has not had |   This issue has been automatically marked as stale because it has not had | ||||||
|   recent activity. It will be closed if no further activity occurs. Thank you |   recent activity. It will be closed if no further activity occurs. Thank you | ||||||
|   for your contributions. |   for your contributions. | ||||||
|  | only: issues | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -29,6 +29,7 @@ jobs: | |||||||
|           - bandit |           - bandit | ||||||
|           - pyright |           - pyright | ||||||
|           - pending-migrations |           - pending-migrations | ||||||
|  |           - codespell | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
| @ -59,7 +60,7 @@ jobs: | |||||||
|           cp authentik/lib/default.yml local.env.yml |           cp authentik/lib/default.yml local.env.yml | ||||||
|           cp -R .github .. |           cp -R .github .. | ||||||
|           cp -R scripts .. |           cp -R scripts .. | ||||||
|           git checkout $(git describe --abbrev=0 --match 'version/*') |           git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) | ||||||
|           rm -rf .github/ scripts/ |           rm -rf .github/ scripts/ | ||||||
|           mv ../.github ../scripts . |           mv ../.github ../scripts . | ||||||
|       - name: Setup authentik env (ensure stable deps are installed) |       - name: Setup authentik env (ensure stable deps are installed) | ||||||
| @ -79,12 +80,21 @@ jobs: | |||||||
|       - name: migrate to latest |       - name: migrate to latest | ||||||
|         run: poetry run python -m lifecycle.migrate |         run: poetry run python -m lifecycle.migrate | ||||||
|   test-unittest: |   test-unittest: | ||||||
|  |     name: test-unittest - PostgreSQL ${{ matrix.psql }} | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     timeout-minutes: 30 |     timeout-minutes: 30 | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         psql: | ||||||
|  |           - 11-alpine | ||||||
|  |           - 12-alpine | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|       - name: Setup authentik env |       - name: Setup authentik env | ||||||
|         uses: ./.github/actions/setup |         uses: ./.github/actions/setup | ||||||
|  |         with: | ||||||
|  |           postgresql_tag: ${{ matrix.psql }} | ||||||
|       - name: run unittest |       - name: run unittest | ||||||
|         run: | |         run: | | ||||||
|           poetry run make test |           poetry run make test | ||||||
| @ -128,6 +138,8 @@ jobs: | |||||||
|             glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml* |             glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml* | ||||||
|           - name: ldap |           - name: ldap | ||||||
|             glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap* |             glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap* | ||||||
|  |           - name: radius | ||||||
|  |             glob: tests/e2e/test_provider_radius* | ||||||
|           - name: flows |           - name: flows | ||||||
|             glob: tests/e2e/test_flows* |             glob: tests/e2e/test_flows* | ||||||
|     steps: |     steps: | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -15,7 +15,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|       - uses: actions/setup-go@v3 |       - uses: actions/setup-go@v4 | ||||||
|         with: |         with: | ||||||
|           go-version: "^1.17" |           go-version: "^1.17" | ||||||
|       - name: Prepare and generate API |       - name: Prepare and generate API | ||||||
| @ -34,7 +34,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|       - uses: actions/setup-go@v3 |       - uses: actions/setup-go@v4 | ||||||
|         with: |         with: | ||||||
|           go-version: "^1.17" |           go-version: "^1.17" | ||||||
|       - name: Generate API |       - name: Generate API | ||||||
| @ -59,8 +59,9 @@ jobs: | |||||||
|         type: |         type: | ||||||
|           - proxy |           - proxy | ||||||
|           - ldap |           - ldap | ||||||
|  |           - radius | ||||||
|         arch: |         arch: | ||||||
|           - 'linux/amd64' |           - "linux/amd64" | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
| @ -106,17 +107,18 @@ jobs: | |||||||
|         type: |         type: | ||||||
|           - proxy |           - proxy | ||||||
|           - ldap |           - ldap | ||||||
|  |           - radius | ||||||
|         goos: [linux] |         goos: [linux] | ||||||
|         goarch: [amd64, arm64] |         goarch: [amd64, arm64] | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|       - uses: actions/setup-go@v3 |       - uses: actions/setup-go@v4 | ||||||
|         with: |         with: | ||||||
|           go-version: "^1.17" |           go-version: "^1.17" | ||||||
|       - uses: actions/setup-node@v3.6.0 |       - uses: actions/setup-node@v3.6.0 | ||||||
|         with: |         with: | ||||||
|           node-version: '18' |           node-version: "18" | ||||||
|           cache: 'npm' |           cache: "npm" | ||||||
|           cache-dependency-path: web/package-lock.json |           cache-dependency-path: web/package-lock.json | ||||||
|       - name: Generate API |       - name: Generate API | ||||||
|         run: make gen-client-go |         run: make gen-client-go | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							| @ -39,10 +39,32 @@ jobs: | |||||||
|       - name: test |       - name: test | ||||||
|         working-directory: website/ |         working-directory: website/ | ||||||
|         run: npm test |         run: npm test | ||||||
|  |   build: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     name: ${{ matrix.job }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         job: | ||||||
|  |           - build | ||||||
|  |           - build-docs-only | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v3 | ||||||
|  |       - uses: actions/setup-node@v3.6.0 | ||||||
|  |         with: | ||||||
|  |           node-version: '18' | ||||||
|  |           cache: 'npm' | ||||||
|  |           cache-dependency-path: website/package-lock.json | ||||||
|  |       - working-directory: website/ | ||||||
|  |         run: npm ci | ||||||
|  |       - name: build | ||||||
|  |         working-directory: website/ | ||||||
|  |         run: npm run ${{ matrix.job }} | ||||||
|   ci-website-mark: |   ci-website-mark: | ||||||
|     needs: |     needs: | ||||||
|       - lint-prettier |       - lint-prettier | ||||||
|       - test |       - test | ||||||
|  |       - build | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - run: echo mark |       - run: echo mark | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
								
							| @ -10,6 +10,11 @@ jobs: | |||||||
|     name: Delete old unused container images |     name: Delete old unused container images | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|  |       - id: generate_token | ||||||
|  |         uses: tibdex/github-app-token@v1 | ||||||
|  |         with: | ||||||
|  |           app_id: ${{ secrets.GH_APP_ID }} | ||||||
|  |           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||||
|       - name: Delete 'dev' containers older than a week |       - name: Delete 'dev' containers older than a week | ||||||
|         uses: snok/container-retention-policy@v2 |         uses: snok/container-retention-policy@v2 | ||||||
|         with: |         with: | ||||||
| @ -18,5 +23,5 @@ jobs: | |||||||
|           account-type: org |           account-type: org | ||||||
|           org-name: goauthentik |           org-name: goauthentik | ||||||
|           untagged-only: false |           untagged-only: false | ||||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|           skip-tags: gh-next,gh-main |           skip-tags: gh-next,gh-main | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -52,9 +52,10 @@ jobs: | |||||||
|         type: |         type: | ||||||
|           - proxy |           - proxy | ||||||
|           - ldap |           - ldap | ||||||
|  |           - radius | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|       - uses: actions/setup-go@v3 |       - uses: actions/setup-go@v4 | ||||||
|         with: |         with: | ||||||
|           go-version: "^1.17" |           go-version: "^1.17" | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
| @ -99,11 +100,12 @@ jobs: | |||||||
|         type: |         type: | ||||||
|           - proxy |           - proxy | ||||||
|           - ldap |           - ldap | ||||||
|  |           - radius | ||||||
|         goos: [linux, darwin] |         goos: [linux, darwin] | ||||||
|         goarch: [amd64, arm64] |         goarch: [amd64, arm64] | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|       - uses: actions/setup-go@v3 |       - uses: actions/setup-go@v4 | ||||||
|         with: |         with: | ||||||
|           go-version: "^1.17" |           go-version: "^1.17" | ||||||
|       - uses: actions/setup-node@v3.6.0 |       - uses: actions/setup-node@v3.6.0 | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -22,18 +22,23 @@ jobs: | |||||||
|           docker-compose up --no-start |           docker-compose up --no-start | ||||||
|           docker-compose start postgresql redis |           docker-compose start postgresql redis | ||||||
|           docker-compose run -u root server test-all |           docker-compose run -u root server test-all | ||||||
|  |       - id: generate_token | ||||||
|  |         uses: tibdex/github-app-token@v1 | ||||||
|  |         with: | ||||||
|  |           app_id: ${{ secrets.GH_APP_ID }} | ||||||
|  |           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||||
|       - name: Extract version number |       - name: Extract version number | ||||||
|         id: get_version |         id: get_version | ||||||
|         uses: actions/github-script@v6 |         uses: actions/github-script@v6 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.BOT_GITHUB_TOKEN }} |           github-token: ${{ steps.generate_token.outputs.token }} | ||||||
|           script: | |           script: | | ||||||
|             return context.payload.ref.replace(/\/refs\/tags\/version\//, ''); |             return context.payload.ref.replace(/\/refs\/tags\/version\//, ''); | ||||||
|       - name: Create Release |       - name: Create Release | ||||||
|         id: create_release |         id: create_release | ||||||
|         uses: actions/create-release@v1.1.4 |         uses: actions/create-release@v1.1.4 | ||||||
|         env: |         env: | ||||||
|           GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} |           GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} | ||||||
|         with: |         with: | ||||||
|           tag_name: ${{ github.ref }} |           tag_name: ${{ github.ref }} | ||||||
|           release_name: Release ${{ steps.get_version.outputs.result }} |           release_name: Release ${{ steps.get_version.outputs.result }} | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							| @ -18,18 +18,23 @@ jobs: | |||||||
|   compile: |   compile: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|  |       - id: generate_token | ||||||
|  |         uses: tibdex/github-app-token@v1 | ||||||
|  |         with: | ||||||
|  |           app_id: ${{ secrets.GH_APP_ID }} | ||||||
|  |           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|       - name: Setup authentik env |       - name: Setup authentik env | ||||||
|         uses: ./.github/actions/setup |         uses: ./.github/actions/setup | ||||||
|       - name: run compile |       - name: run compile | ||||||
|         run: poetry run ./manage.py compilemessages |         run: poetry run ./manage.py compilemessages | ||||||
|       - name: Create Pull Request |       - name: Create Pull Request | ||||||
|         uses: peter-evans/create-pull-request@v4 |         uses: peter-evans/create-pull-request@v5 | ||||||
|         id: cpr |         id: cpr | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} |           token: ${{ steps.generate_token.outputs.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" | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -9,9 +9,14 @@ jobs: | |||||||
|   build: |   build: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|  |       - id: generate_token | ||||||
|  |         uses: tibdex/github-app-token@v1 | ||||||
|  |         with: | ||||||
|  |           app_id: ${{ secrets.GH_APP_ID }} | ||||||
|  |           private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} | ||||||
|       - uses: actions/checkout@v3 |       - uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|       - uses: actions/setup-node@v3.6.0 |       - uses: actions/setup-node@v3.6.0 | ||||||
|         with: |         with: | ||||||
|           node-version: '18' |           node-version: '18' | ||||||
| @ -30,10 +35,10 @@ jobs: | |||||||
|         run: | |         run: | | ||||||
|           export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'` |           export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'` | ||||||
|           npm i @goauthentik/api@$VERSION |           npm i @goauthentik/api@$VERSION | ||||||
|       - uses: peter-evans/create-pull-request@v4 |       - uses: peter-evans/create-pull-request@v5 | ||||||
|         id: cpr |         id: cpr | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|           branch: update-web-api-client |           branch: update-web-api-client | ||||||
|           commit-message: "web: bump API Client version" |           commit-message: "web: bump API Client version" | ||||||
|           title: "web: bump API Client version" |           title: "web: bump API Client version" | ||||||
| @ -42,8 +47,8 @@ jobs: | |||||||
|           signoff: true |           signoff: true | ||||||
|           team-reviewers: "@goauthentik/core" |           team-reviewers: "@goauthentik/core" | ||||||
|           author: authentik bot <github-bot@goauthentik.io> |           author: authentik bot <github-bot@goauthentik.io> | ||||||
|       - uses: peter-evans/enable-pull-request-automerge@v2 |       - uses: peter-evans/enable-pull-request-automerge@v3 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.BOT_GITHUB_TOKEN }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|           pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} |           pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} | ||||||
|           merge-method: squash |           merge-method: squash | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ The following is a set of guidelines for contributing to authentik and its compo | |||||||
| -   [Reporting Bugs](#reporting-bugs) | -   [Reporting Bugs](#reporting-bugs) | ||||||
| -   [Suggesting Enhancements](#suggesting-enhancements) | -   [Suggesting Enhancements](#suggesting-enhancements) | ||||||
| -   [Your First Code Contribution](#your-first-code-contribution) | -   [Your First Code Contribution](#your-first-code-contribution) | ||||||
|  | -   [Help with the Docs](#help-with-the-docs) | ||||||
| -   [Pull Requests](#pull-requests) | -   [Pull Requests](#pull-requests) | ||||||
|  |  | ||||||
| [Styleguides](#styleguides) | [Styleguides](#styleguides) | ||||||
| @ -135,6 +136,9 @@ authentik can be run locally, all though depending on which part you want to wor | |||||||
|  |  | ||||||
| This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github) | This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github) | ||||||
|  |  | ||||||
|  | ### Help with the Docs | ||||||
|  | Contributions to the technical documentation are greatly appreciated. Open a PR if you have improvements to make or new content to add. If you have questions or suggestions about the documentation, open an Issue. No contribution is too small. | ||||||
|  |  | ||||||
| ### Pull Requests | ### Pull Requests | ||||||
|  |  | ||||||
| The process described here has several goals: | The process described here has several goals: | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -20,7 +20,7 @@ WORKDIR /work/web | |||||||
| RUN npm ci && npm run build | RUN npm ci && npm run build | ||||||
|  |  | ||||||
| # Stage 3: Poetry to requirements.txt export | # Stage 3: Poetry to requirements.txt export | ||||||
| FROM docker.io/python:3.11.2-slim-bullseye AS poetry-locker | FROM docker.io/python:3.11.3-slim-bullseye AS poetry-locker | ||||||
|  |  | ||||||
| WORKDIR /work | WORKDIR /work | ||||||
| COPY ./pyproject.toml /work | COPY ./pyproject.toml /work | ||||||
| @ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \ | |||||||
|     poetry export -f requirements.txt --dev --output requirements-dev.txt |     poetry export -f requirements.txt --dev --output requirements-dev.txt | ||||||
|  |  | ||||||
| # Stage 4: Build go proxy | # Stage 4: Build go proxy | ||||||
| FROM docker.io/golang:1.20.2-bullseye AS go-builder | FROM docker.io/golang:1.20.3-bullseye AS go-builder | ||||||
|  |  | ||||||
| WORKDIR /work | WORKDIR /work | ||||||
|  |  | ||||||
| @ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum | |||||||
| RUN go build -o /work/authentik ./cmd/server/ | RUN go build -o /work/authentik ./cmd/server/ | ||||||
|  |  | ||||||
| # Stage 5: MaxMind GeoIP | # Stage 5: MaxMind GeoIP | ||||||
| FROM docker.io/maxmindinc/geoipupdate:v4.10 as geoip | FROM docker.io/maxmindinc/geoipupdate:v5.0 as geoip | ||||||
|  |  | ||||||
| ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City" | ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City" | ||||||
| ENV GEOIPUPDATE_VERBOSE="true" | ENV GEOIPUPDATE_VERBOSE="true" | ||||||
| @ -62,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | |||||||
|     " |     " | ||||||
|  |  | ||||||
| # Stage 6: Run | # Stage 6: Run | ||||||
| FROM docker.io/python:3.11.2-slim-bullseye AS final-image | FROM docker.io/python:3.11.3-slim-bullseye AS final-image | ||||||
|  |  | ||||||
| LABEL org.opencontainers.image.url https://goauthentik.io | LABEL org.opencontainers.image.url https://goauthentik.io | ||||||
| LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info. | LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info. | ||||||
| @ -102,7 +102,7 @@ COPY ./tests /tests | |||||||
| COPY ./manage.py / | COPY ./manage.py / | ||||||
| COPY ./blueprints /blueprints | COPY ./blueprints /blueprints | ||||||
| COPY ./lifecycle/ /lifecycle | COPY ./lifecycle/ /lifecycle | ||||||
| COPY --from=go-builder /work/authentik /authentik-proxy | COPY --from=go-builder /work/authentik /bin/authentik | ||||||
| COPY --from=web-builder /work/web/dist/ /web/dist/ | COPY --from=web-builder /work/web/dist/ /web/dist/ | ||||||
| COPY --from=web-builder /work/web/authentik/ /web/authentik/ | COPY --from=web-builder /work/web/authentik/ /web/authentik/ | ||||||
| COPY --from=website-builder /work/website/help/ /website/help/ | COPY --from=website-builder /work/website/help/ /website/help/ | ||||||
|  | |||||||
							
								
								
									
										57
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								Makefile
									
									
									
									
									
								
							| @ -4,6 +4,20 @@ UID = $(shell id -u) | |||||||
| GID = $(shell id -g) | GID = $(shell id -g) | ||||||
| NPM_VERSION = $(shell python -m scripts.npm_version) | NPM_VERSION = $(shell python -m scripts.npm_version) | ||||||
|  |  | ||||||
|  | CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \ | ||||||
|  | 		-I .github/codespell-words.txt \ | ||||||
|  | 		-S 'web/src/locales/**' \ | ||||||
|  | 		authentik \ | ||||||
|  | 		internal \ | ||||||
|  | 		cmd \ | ||||||
|  | 		web/src \ | ||||||
|  | 		website/src \ | ||||||
|  | 		website/blog \ | ||||||
|  | 		website/developer-docs \ | ||||||
|  | 		website/docs \ | ||||||
|  | 		website/integrations \ | ||||||
|  | 		website/src | ||||||
|  |  | ||||||
| all: lint-fix lint test gen web | all: lint-fix lint test gen web | ||||||
|  |  | ||||||
| test-go: | test-go: | ||||||
| @ -26,14 +40,7 @@ test: | |||||||
| lint-fix: | lint-fix: | ||||||
| 	isort authentik tests scripts lifecycle | 	isort authentik tests scripts lifecycle | ||||||
| 	black authentik tests scripts lifecycle | 	black authentik tests scripts lifecycle | ||||||
| 	codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \ | 	codespell -w $(CODESPELL_ARGS) | ||||||
| 		authentik \ |  | ||||||
| 		internal \ |  | ||||||
| 		cmd \ |  | ||||||
| 		web/src \ |  | ||||||
| 		website/src \ |  | ||||||
| 		website/docs \ |  | ||||||
| 		website/developer-docs |  | ||||||
|  |  | ||||||
| lint: | lint: | ||||||
| 	pylint authentik tests lifecycle | 	pylint authentik tests lifecycle | ||||||
| @ -43,9 +50,6 @@ lint: | |||||||
| migrate: | migrate: | ||||||
| 	python -m lifecycle.migrate | 	python -m lifecycle.migrate | ||||||
|  |  | ||||||
| run: |  | ||||||
| 	go run -v ./cmd/server/ |  | ||||||
|  |  | ||||||
| i18n-extract: i18n-extract-core web-extract | i18n-extract: i18n-extract-core web-extract | ||||||
|  |  | ||||||
| i18n-extract-core: | i18n-extract-core: | ||||||
| @ -59,15 +63,20 @@ gen-build: | |||||||
| 	AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json | 	AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json | ||||||
| 	AUTHENTIK_DEBUG=true ak spectacular --file schema.yml | 	AUTHENTIK_DEBUG=true ak spectacular --file schema.yml | ||||||
|  |  | ||||||
|  | gen-changelog: | ||||||
|  | 	git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md | ||||||
|  | 	npx prettier --write changelog.md | ||||||
|  |  | ||||||
| gen-diff: | gen-diff: | ||||||
| 	git show $(shell git describe --abbrev=0):schema.yml > old_schema.yml | 	git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml | ||||||
| 	docker run \ | 	docker run \ | ||||||
| 		--rm -v ${PWD}:/local \ | 		--rm -v ${PWD}:/local \ | ||||||
| 		--user ${UID}:${GID} \ | 		--user ${UID}:${GID} \ | ||||||
| 		docker.io/openapitools/openapi-diff:2.1.0-beta.3 \ | 		docker.io/openapitools/openapi-diff:2.1.0-beta.6 \ | ||||||
| 		--markdown /local/diff.md \ | 		--markdown /local/diff.md \ | ||||||
| 		/local/old_schema.yml /local/schema.yml | 		/local/old_schema.yml /local/schema.yml | ||||||
| 	rm old_schema.yml | 	rm old_schema.yml | ||||||
|  | 	npx prettier --write diff.md | ||||||
|  |  | ||||||
| gen-clean: | gen-clean: | ||||||
| 	rm -rf web/api/src/ | 	rm -rf web/api/src/ | ||||||
| @ -77,7 +86,7 @@ gen-client-ts: | |||||||
| 	docker run \ | 	docker run \ | ||||||
| 		--rm -v ${PWD}:/local \ | 		--rm -v ${PWD}:/local \ | ||||||
| 		--user ${UID}:${GID} \ | 		--user ${UID}:${GID} \ | ||||||
| 		docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \ | 		docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \ | ||||||
| 		-i /local/schema.yml \ | 		-i /local/schema.yml \ | ||||||
| 		-g typescript-fetch \ | 		-g typescript-fetch \ | ||||||
| 		-o /local/gen-ts-api \ | 		-o /local/gen-ts-api \ | ||||||
| @ -90,20 +99,21 @@ gen-client-ts: | |||||||
| 	\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api | 	\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api | ||||||
|  |  | ||||||
| gen-client-go: | gen-client-go: | ||||||
| 	wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml | 	mkdir -p ./gen-go-api ./gen-go-api/templates | ||||||
| 	mkdir -p templates | 	wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./gen-go-api/config.yaml | ||||||
| 	wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache | 	wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./gen-go-api/templates/README.mustache | ||||||
| 	wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache | 	wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./gen-go-api/templates/go.mod.mustache | ||||||
|  | 	cp schema.yml ./gen-go-api/ | ||||||
| 	docker run \ | 	docker run \ | ||||||
| 		--rm -v ${PWD}:/local \ | 		--rm -v ${PWD}/gen-go-api:/local \ | ||||||
| 		--user ${UID}:${GID} \ | 		--user ${UID}:${GID} \ | ||||||
| 		docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \ | 		docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \ | ||||||
| 		-i /local/schema.yml \ | 		-i /local/schema.yml \ | ||||||
| 		-g go \ | 		-g go \ | ||||||
| 		-o /local/gen-go-api \ | 		-o /local/ \ | ||||||
| 		-c /local/config.yaml | 		-c /local/config.yaml | ||||||
| 	go mod edit -replace goauthentik.io/api/v3=./gen-go-api | 	go mod edit -replace goauthentik.io/api/v3=./gen-go-api | ||||||
| 	rm -rf config.yaml ./templates/ | 	rm -rf ./gen-go-api/config.yaml ./gen-go-api/templates/ | ||||||
|  |  | ||||||
| gen-dev-config: | gen-dev-config: | ||||||
| 	python -m scripts.generate_config | 	python -m scripts.generate_config | ||||||
| @ -172,6 +182,9 @@ ci-pylint: ci--meta-debug | |||||||
| ci-black: ci--meta-debug | ci-black: ci--meta-debug | ||||||
| 	black --check $(PY_SOURCES) | 	black --check $(PY_SOURCES) | ||||||
|  |  | ||||||
|  | ci-codespell: ci--meta-debug | ||||||
|  | 	codespell $(CODESPELL_ARGS) -s | ||||||
|  |  | ||||||
| ci-isort: ci--meta-debug | ci-isort: ci--meta-debug | ||||||
| 	isort --check $(PY_SOURCES) | 	isort --check $(PY_SOURCES) | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							| @ -15,13 +15,13 @@ | |||||||
|  |  | ||||||
| ## What is authentik? | ## What is authentik? | ||||||
|  |  | ||||||
| authentik is an open-source Identity Provider focused on flexibility and versatility. You can use authentik in an existing environment to add support for new protocols. authentik is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it. | Authentik is an open-source Identity Provider that emphasizes flexibility and versatility. It can be seamlessly integrated into existing environments to support new protocols. Authentik is also a great solution for implementing sign-up, recovery, and other similar features in your application, saving you the hassle of dealing with them. | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
| For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github) | For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github). | ||||||
|  |  | ||||||
| For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github) | For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github). | ||||||
|  |  | ||||||
| ## Screenshots | ## Screenshots | ||||||
|  |  | ||||||
| @ -32,15 +32,15 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h | |||||||
|  |  | ||||||
| ## Development | ## Development | ||||||
|  |  | ||||||
| See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github) | See [Developer Documentation](https://goauthentik.io/developer-docs/?utm_source=github) | ||||||
|  |  | ||||||
| ## Security | ## Security | ||||||
|  |  | ||||||
| See [SECURITY.md](SECURITY.md) | See [SECURITY.md](SECURITY.md) | ||||||
|  |  | ||||||
| ## Support | ## Adoption and Contributions | ||||||
|  |  | ||||||
| Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! | Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md). | ||||||
|  |  | ||||||
| ## Sponsors | ## Sponsors | ||||||
|  |  | ||||||
|  | |||||||
| @ -6,9 +6,8 @@ Authentik takes security very seriously. We follow the rules of [responsible dis | |||||||
|  |  | ||||||
| | Version   | Supported          | | | Version   | Supported          | | ||||||
| | --------- | ------------------ | | | --------- | ------------------ | | ||||||
| | 2022.12.x | :white_check_mark: | |  | ||||||
| | 2023.1.x  | :white_check_mark: | |  | ||||||
| | 2023.2.x  | :white_check_mark: | | | 2023.2.x  | :white_check_mark: | | ||||||
|  | | 2023.3.x  | :white_check_mark: | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from os import environ | from os import environ | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| __version__ = "2023.3.0" | __version__ = "2023.4.2" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| """authentik administration overview""" | """authentik administration overview""" | ||||||
| import os |  | ||||||
| import platform | import platform | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from sys import version as python_version | from sys import version as python_version | ||||||
| @ -34,7 +33,6 @@ class RuntimeDict(TypedDict): | |||||||
| class SystemSerializer(PassiveSerializer): | class SystemSerializer(PassiveSerializer): | ||||||
|     """Get system information.""" |     """Get system information.""" | ||||||
|  |  | ||||||
|     env = SerializerMethodField() |  | ||||||
|     http_headers = SerializerMethodField() |     http_headers = SerializerMethodField() | ||||||
|     http_host = SerializerMethodField() |     http_host = SerializerMethodField() | ||||||
|     http_is_secure = SerializerMethodField() |     http_is_secure = SerializerMethodField() | ||||||
| @ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer): | |||||||
|     server_time = SerializerMethodField() |     server_time = SerializerMethodField() | ||||||
|     embedded_outpost_host = SerializerMethodField() |     embedded_outpost_host = SerializerMethodField() | ||||||
|  |  | ||||||
|     def get_env(self, request: Request) -> dict[str, str]: |  | ||||||
|         """Get Environment""" |  | ||||||
|         return os.environ.copy() |  | ||||||
|  |  | ||||||
|     def get_http_headers(self, request: Request) -> dict[str, str]: |     def get_http_headers(self, request: Request) -> dict[str, str]: | ||||||
|         """Get HTTP Request headers""" |         """Get HTTP Request headers""" | ||||||
|         headers = {} |         headers = {} | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """API Authentication""" | """API Authentication""" | ||||||
|  | from hmac import compare_digest | ||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]: | |||||||
|     and return the service account for the managed outpost""" |     and return the service account for the managed outpost""" | ||||||
|     from authentik.outposts.apps import MANAGED_OUTPOST |     from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
|  |  | ||||||
|     if value != settings.SECRET_KEY: |     if not compare_digest(value, settings.SECRET_KEY): | ||||||
|         return None |         return None | ||||||
|     outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) |     outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST) | ||||||
|     if not outposts: |     if not outposts: | ||||||
|  | |||||||
| @ -7,82 +7,13 @@ API Browser - {{ tenant.branding_title }} | |||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block head %} | {% block head %} | ||||||
| <script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script> | <script src="{% static 'dist/standalone/api-browser/index.js' %}?version={{ version }}" type="module"></script> | ||||||
| <script> | <meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)"> | ||||||
| function getCookie(name) { | <meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)"> | ||||||
|     let cookieValue = ""; | <link rel="icon" href="{{ tenant.branding_favicon }}"> | ||||||
|     if (document.cookie && document.cookie !== "") { | <link rel="shortcut icon" href="{{ tenant.branding_favicon }}"> | ||||||
|         const cookies = document.cookie.split(";"); |  | ||||||
|         for (let i = 0; i < cookies.length; i++) { |  | ||||||
|             const cookie = cookies[i].trim(); |  | ||||||
|             // Does this cookie string begin with the name we want? |  | ||||||
|             if (cookie.substring(0, name.length + 1) === name + "=") { |  | ||||||
|                 cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return cookieValue; |  | ||||||
| } |  | ||||||
| window.addEventListener('DOMContentLoaded', (event) => { |  | ||||||
|     const rapidocEl = document.querySelector('rapi-doc'); |  | ||||||
|     rapidocEl.addEventListener('before-try', (e) => { |  | ||||||
|         e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf")); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| <style> |  | ||||||
|     img.logo { |  | ||||||
|         width: 100%; |  | ||||||
|         padding: 1rem 0.5rem 1.5rem 0.5rem; |  | ||||||
|         min-height: 48px; |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block body %} | {% block body %} | ||||||
| <rapi-doc | <ak-api-browser schemaPath="{{ path }}"></ak-api-browser> | ||||||
|     spec-url="{{ path }}" |  | ||||||
|     heading-text="" |  | ||||||
|     theme="light" |  | ||||||
|     render-style="read" |  | ||||||
|     default-schema-tab="schema" |  | ||||||
|     primary-color="#fd4b2d" |  | ||||||
|     nav-bg-color="#212427" |  | ||||||
|     bg-color="#000000" |  | ||||||
|     text-color="#000000" |  | ||||||
|     nav-text-color="#ffffff" |  | ||||||
|     nav-hover-bg-color="#3c3f42" |  | ||||||
|     nav-accent-color="#4f5255" |  | ||||||
|     nav-hover-text-color="#ffffff" |  | ||||||
|     use-path-in-nav-bar="true" |  | ||||||
|     nav-item-spacing="relaxed" |  | ||||||
|     allow-server-selection="false" |  | ||||||
|     show-header="false" |  | ||||||
|     allow-spec-url-load="false" |  | ||||||
|     allow-spec-file-load="false"> |  | ||||||
|     <div slot="nav-logo"> |  | ||||||
|         <img  alt="authentik Logo" class="logo" src="{% static 'dist/assets/icons/icon_left_brand.png' %}" /> |  | ||||||
|     </div> |  | ||||||
| </rapi-doc> |  | ||||||
| <script> |  | ||||||
| const rapidoc = document.querySelector("rapi-doc"); |  | ||||||
| const matcher = window.matchMedia("(prefers-color-scheme: light)"); |  | ||||||
| const changer = (ev) => { |  | ||||||
|     const style = getComputedStyle(document.documentElement); |  | ||||||
|     let bg, text = ""; |  | ||||||
|     if (matcher.matches) { |  | ||||||
|         bg = style.getPropertyValue('--pf-global--BackgroundColor--light-300'); |  | ||||||
|         text = style.getPropertyValue('--pf-global--Color--300'); |  | ||||||
|     } else { |  | ||||||
|         bg = style.getPropertyValue('--ak-dark-background'); |  | ||||||
|         text = style.getPropertyValue('--ak-dark-foreground'); |  | ||||||
|     } |  | ||||||
|     rapidoc.attributes.getNamedItem("bg-color").value = bg.trim(); |  | ||||||
|     rapidoc.attributes.getNamedItem("text-color").value = text.trim(); |  | ||||||
|     rapidoc.requestUpdate(); |  | ||||||
| }; |  | ||||||
| matcher.addEventListener("change", changer); |  | ||||||
| window.addEventListener("load", changer); |  | ||||||
| </script> |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | |||||||
| @ -56,6 +56,7 @@ from authentik.providers.oauth2.api.tokens import ( | |||||||
|     RefreshTokenViewSet, |     RefreshTokenViewSet, | ||||||
| ) | ) | ||||||
| from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet | from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet | ||||||
|  | from authentik.providers.radius.api import RadiusOutpostConfigViewSet, RadiusProviderViewSet | ||||||
| from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet | from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet | ||||||
| from authentik.providers.saml.api.providers import SAMLProviderViewSet | from authentik.providers.saml.api.providers import SAMLProviderViewSet | ||||||
| from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet | from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet | ||||||
| @ -128,6 +129,7 @@ router.register("outposts/service_connections/docker", DockerServiceConnectionVi | |||||||
| router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet) | router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet) | ||||||
| router.register("outposts/proxy", ProxyOutpostConfigViewSet) | router.register("outposts/proxy", ProxyOutpostConfigViewSet) | ||||||
| router.register("outposts/ldap", LDAPOutpostConfigViewSet) | router.register("outposts/ldap", LDAPOutpostConfigViewSet) | ||||||
|  | router.register("outposts/radius", RadiusOutpostConfigViewSet) | ||||||
|  |  | ||||||
| router.register("flows/instances", FlowViewSet) | router.register("flows/instances", FlowViewSet) | ||||||
| router.register("flows/bindings", FlowStageBindingViewSet) | router.register("flows/bindings", FlowStageBindingViewSet) | ||||||
| @ -166,6 +168,7 @@ router.register("providers/proxy", ProxyProviderViewSet) | |||||||
| router.register("providers/oauth2", OAuth2ProviderViewSet) | router.register("providers/oauth2", OAuth2ProviderViewSet) | ||||||
| router.register("providers/saml", SAMLProviderViewSet) | router.register("providers/saml", SAMLProviderViewSet) | ||||||
| router.register("providers/scim", SCIMProviderViewSet) | router.register("providers/scim", SCIMProviderViewSet) | ||||||
|  | router.register("providers/radius", RadiusProviderViewSet) | ||||||
|  |  | ||||||
| router.register("oauth2/authorization_codes", AuthorizationCodeViewSet) | router.register("oauth2/authorization_codes", AuthorizationCodeViewSet) | ||||||
| router.register("oauth2/refresh_tokens", RefreshTokenViewSet) | router.register("oauth2/refresh_tokens", RefreshTokenViewSet) | ||||||
|  | |||||||
| @ -55,11 +55,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig): | |||||||
|         """Load v1 tasks""" |         """Load v1 tasks""" | ||||||
|         self.import_module("authentik.blueprints.v1.tasks") |         self.import_module("authentik.blueprints.v1.tasks") | ||||||
|  |  | ||||||
|     def reconcile_blueprints_discover(self): |     def reconcile_blueprints_discovery(self): | ||||||
|         """Run blueprint discovery""" |         """Run blueprint discovery""" | ||||||
|         from authentik.blueprints.v1.tasks import blueprints_discover, clear_failed_blueprints |         from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints | ||||||
|  |  | ||||||
|         blueprints_discover.delay() |         blueprints_discovery.delay() | ||||||
|         clear_failed_blueprints.delay() |         clear_failed_blueprints.delay() | ||||||
|  |  | ||||||
|     def import_models(self): |     def import_models(self): | ||||||
|  | |||||||
| @ -19,10 +19,8 @@ class Command(BaseCommand): | |||||||
|         for blueprint_path in options.get("blueprints", []): |         for blueprint_path in options.get("blueprints", []): | ||||||
|             content = BlueprintInstance(path=blueprint_path).retrieve() |             content = BlueprintInstance(path=blueprint_path).retrieve() | ||||||
|             importer = Importer(content) |             importer = Importer(content) | ||||||
|             valid, logs = importer.validate() |             valid, _ = importer.validate() | ||||||
|             if not valid: |             if not valid: | ||||||
|                 for log in logs: |  | ||||||
|                     getattr(LOGGER, log.pop("log_level"))(**log) |  | ||||||
|                 self.stderr.write("blueprint invalid") |                 self.stderr.write("blueprint invalid") | ||||||
|                 sys_exit(1) |                 sys_exit(1) | ||||||
|             importer.apply() |             importer.apply() | ||||||
|  | |||||||
| @ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): | |||||||
|     def retrieve_file(self) -> str: |     def retrieve_file(self) -> str: | ||||||
|         """Get blueprint from path""" |         """Get blueprint from path""" | ||||||
|         try: |         try: | ||||||
|             full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path)) |             base = Path(CONFIG.y("blueprints_dir")) | ||||||
|  |             full_path = base.joinpath(Path(self.path)).resolve() | ||||||
|  |             if not str(full_path).startswith(str(base.resolve())): | ||||||
|  |                 raise BlueprintRetrievalFailed("Invalid blueprint path") | ||||||
|             with full_path.open("r", encoding="utf-8") as _file: |             with full_path.open("r", encoding="utf-8") as _file: | ||||||
|                 return _file.read() |                 return _file.read() | ||||||
|         except (IOError, OSError) as exc: |         except (IOError, OSError) as exc: | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ from authentik.lib.utils.time import fqdn_rand | |||||||
|  |  | ||||||
| CELERY_BEAT_SCHEDULE = { | CELERY_BEAT_SCHEDULE = { | ||||||
|     "blueprints_v1_discover": { |     "blueprints_v1_discover": { | ||||||
|         "task": "authentik.blueprints.v1.tasks.blueprints_discover", |         "task": "authentik.blueprints.v1.tasks.blueprints_discovery", | ||||||
|         "schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"), |         "schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| """Blueprint helpers""" | """Blueprint helpers""" | ||||||
| from functools import wraps | from functools import wraps | ||||||
| from pathlib import Path |  | ||||||
| from typing import Callable | from typing import Callable | ||||||
|  |  | ||||||
| from django.apps import apps | from django.apps import apps | ||||||
| @ -45,13 +44,3 @@ def reconcile_app(app_name: str): | |||||||
|         return wrapper |         return wrapper | ||||||
|  |  | ||||||
|     return wrapper_outer |     return wrapper_outer | ||||||
|  |  | ||||||
|  |  | ||||||
| def load_yaml_fixture(path: str, **kwargs) -> str: |  | ||||||
|     """Load yaml fixture, optionally formatting it with kwargs""" |  | ||||||
|     with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture: |  | ||||||
|         fixture = _fixture.read() |  | ||||||
|         try: |  | ||||||
|             return fixture % kwargs |  | ||||||
|         except TypeError: |  | ||||||
|             return fixture |  | ||||||
|  | |||||||
| @ -1,34 +1,15 @@ | |||||||
| """authentik managed models tests""" | """authentik managed models tests""" | ||||||
| from typing import Callable, Type |  | ||||||
|  |  | ||||||
| from django.apps import apps |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.v1.importer import is_model_allowed | from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed | ||||||
| from authentik.lib.models import SerializerModel | from authentik.lib.generators import generate_id | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestModels(TestCase): | class TestModels(TestCase): | ||||||
|     """Test Models""" |     """Test Models""" | ||||||
|  |  | ||||||
|  |     def test_retrieve_file(self): | ||||||
| def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable: |         """Test retrieve_file""" | ||||||
|     """Test serializer""" |         instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts") | ||||||
|  |         with self.assertRaises(BlueprintRetrievalFailed): | ||||||
|     def tester(self: TestModels): |             instance.retrieve() | ||||||
|         if test_model._meta.abstract:  # pragma: no cover |  | ||||||
|             return |  | ||||||
|         model_class = test_model() |  | ||||||
|         self.assertTrue(isinstance(model_class, SerializerModel)) |  | ||||||
|         self.assertIsNotNone(model_class.serializer) |  | ||||||
|  |  | ||||||
|     return tester |  | ||||||
|  |  | ||||||
|  |  | ||||||
| for app in apps.get_app_configs(): |  | ||||||
|     if not app.label.startswith("authentik"): |  | ||||||
|         continue |  | ||||||
|     for model in app.get_models(): |  | ||||||
|         if not is_model_allowed(model): |  | ||||||
|             continue |  | ||||||
|         setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) |  | ||||||
|  | |||||||
							
								
								
									
										34
									
								
								authentik/blueprints/tests/test_serializer_models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								authentik/blueprints/tests/test_serializer_models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | """authentik managed models tests""" | ||||||
|  | from typing import Callable, Type | ||||||
|  |  | ||||||
|  | from django.apps import apps | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.blueprints.v1.importer import is_model_allowed | ||||||
|  | from authentik.lib.models import SerializerModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestModels(TestCase): | ||||||
|  |     """Test Models""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable: | ||||||
|  |     """Test serializer""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         if test_model._meta.abstract:  # pragma: no cover | ||||||
|  |             return | ||||||
|  |         model_class = test_model() | ||||||
|  |         self.assertTrue(isinstance(model_class, SerializerModel)) | ||||||
|  |         self.assertIsNotNone(model_class.serializer) | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for app in apps.get_app_configs(): | ||||||
|  |     if not app.label.startswith("authentik"): | ||||||
|  |         continue | ||||||
|  |     for model in app.get_models(): | ||||||
|  |         if not is_model_allowed(model): | ||||||
|  |             continue | ||||||
|  |         setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model)) | ||||||
| @ -3,12 +3,12 @@ from os import environ | |||||||
|  |  | ||||||
| from django.test import TransactionTestCase | from django.test import TransactionTestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import load_yaml_fixture |  | ||||||
| from authentik.blueprints.v1.exporter import FlowExporter | from authentik.blueprints.v1.exporter import FlowExporter | ||||||
| from authentik.blueprints.v1.importer import Importer, transaction_rollback | from authentik.blueprints.v1.importer import Importer, transaction_rollback | ||||||
| from authentik.core.models import Group | from authentik.core.models import Group | ||||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.lib.tests.utils import load_fixture | ||||||
| from authentik.policies.expression.models import ExpressionPolicy | from authentik.policies.expression.models import ExpressionPolicy | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| @ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase): | |||||||
|         """Test export and import it twice""" |         """Test export and import it twice""" | ||||||
|         count_initial = Prompt.objects.filter(field_key="username").count() |         count_initial = Prompt.objects.filter(field_key="username").count() | ||||||
|  |  | ||||||
|         importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml")) |         importer = Importer(load_fixture("fixtures/static_prompt_export.yaml")) | ||||||
|         self.assertTrue(importer.validate()[0]) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|  |  | ||||||
|         count_before = Prompt.objects.filter(field_key="username").count() |         count_before = Prompt.objects.filter(field_key="username").count() | ||||||
|         self.assertEqual(count_initial + 1, count_before) |         self.assertEqual(count_initial + 1, count_before) | ||||||
|  |  | ||||||
|         importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml")) |         importer = Importer(load_fixture("fixtures/static_prompt_export.yaml")) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|  |  | ||||||
|         self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) |         self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before) | ||||||
| @ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase): | |||||||
|         ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete() |         ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete() | ||||||
|         Group.objects.filter(name="test").delete() |         Group.objects.filter(name="test").delete() | ||||||
|         environ["foo"] = generate_id() |         environ["foo"] = generate_id() | ||||||
|         importer = Importer(load_yaml_fixture("fixtures/tags.yaml"), {"bar": "baz"}) |         importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"}) | ||||||
|         self.assertTrue(importer.validate()[0]) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|         policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first() |         policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first() | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| """Test blueprints v1""" | """Test blueprints v1""" | ||||||
| from django.test import TransactionTestCase | from django.test import TransactionTestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import load_yaml_fixture |  | ||||||
| from authentik.blueprints.v1.importer import Importer | from authentik.blueprints.v1.importer import Importer | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.lib.tests.utils import load_fixture | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestBlueprintsV1Conditions(TransactionTestCase): | class TestBlueprintsV1Conditions(TransactionTestCase): | ||||||
| @ -14,7 +14,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase): | |||||||
|         """Test conditions fulfilled""" |         """Test conditions fulfilled""" | ||||||
|         flow_slug1 = generate_id() |         flow_slug1 = generate_id() | ||||||
|         flow_slug2 = generate_id() |         flow_slug2 = generate_id() | ||||||
|         import_yaml = load_yaml_fixture( |         import_yaml = load_fixture( | ||||||
|             "fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 |             "fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @ -31,7 +31,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase): | |||||||
|         """Test conditions not fulfilled""" |         """Test conditions not fulfilled""" | ||||||
|         flow_slug1 = generate_id() |         flow_slug1 = generate_id() | ||||||
|         flow_slug2 = generate_id() |         flow_slug2 = generate_id() | ||||||
|         import_yaml = load_yaml_fixture( |         import_yaml = load_fixture( | ||||||
|             "fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 |             "fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2 | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| """Test blueprints v1""" | """Test blueprints v1""" | ||||||
| from django.test import TransactionTestCase | from django.test import TransactionTestCase | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import load_yaml_fixture |  | ||||||
| from authentik.blueprints.v1.importer import Importer | from authentik.blueprints.v1.importer import Importer | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.lib.tests.utils import load_fixture | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestBlueprintsV1State(TransactionTestCase): | class TestBlueprintsV1State(TransactionTestCase): | ||||||
| @ -13,7 +13,7 @@ class TestBlueprintsV1State(TransactionTestCase): | |||||||
|     def test_state_present(self): |     def test_state_present(self): | ||||||
|         """Test state present""" |         """Test state present""" | ||||||
|         flow_slug = generate_id() |         flow_slug = generate_id() | ||||||
|         import_yaml = load_yaml_fixture("fixtures/state_present.yaml", id=flow_slug) |         import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug) | ||||||
|  |  | ||||||
|         importer = Importer(import_yaml) |         importer = Importer(import_yaml) | ||||||
|         self.assertTrue(importer.validate()[0]) |         self.assertTrue(importer.validate()[0]) | ||||||
| @ -39,7 +39,7 @@ class TestBlueprintsV1State(TransactionTestCase): | |||||||
|     def test_state_created(self): |     def test_state_created(self): | ||||||
|         """Test state created""" |         """Test state created""" | ||||||
|         flow_slug = generate_id() |         flow_slug = generate_id() | ||||||
|         import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug) |         import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug) | ||||||
|  |  | ||||||
|         importer = Importer(import_yaml) |         importer = Importer(import_yaml) | ||||||
|         self.assertTrue(importer.validate()[0]) |         self.assertTrue(importer.validate()[0]) | ||||||
| @ -65,7 +65,7 @@ class TestBlueprintsV1State(TransactionTestCase): | |||||||
|     def test_state_absent(self): |     def test_state_absent(self): | ||||||
|         """Test state absent""" |         """Test state absent""" | ||||||
|         flow_slug = generate_id() |         flow_slug = generate_id() | ||||||
|         import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug) |         import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug) | ||||||
|  |  | ||||||
|         importer = Importer(import_yaml) |         importer = Importer(import_yaml) | ||||||
|         self.assertTrue(importer.validate()[0]) |         self.assertTrue(importer.validate()[0]) | ||||||
| @ -74,7 +74,7 @@ class TestBlueprintsV1State(TransactionTestCase): | |||||||
|         flow: Flow = Flow.objects.filter(slug=flow_slug).first() |         flow: Flow = Flow.objects.filter(slug=flow_slug).first() | ||||||
|         self.assertEqual(flow.slug, flow_slug) |         self.assertEqual(flow.slug, flow_slug) | ||||||
|  |  | ||||||
|         import_yaml = load_yaml_fixture("fixtures/state_absent.yaml", id=flow_slug) |         import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug) | ||||||
|         importer = Importer(import_yaml) |         importer = Importer(import_yaml) | ||||||
|         self.assertTrue(importer.validate()[0]) |         self.assertTrue(importer.validate()[0]) | ||||||
|         self.assertTrue(importer.apply()) |         self.assertTrue(importer.apply()) | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from django.test import TransactionTestCase | |||||||
| from yaml import dump | from yaml import dump | ||||||
|  |  | ||||||
| from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus | from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus | ||||||
| from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find | from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discovery, blueprints_find | ||||||
| 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 | ||||||
|  |  | ||||||
| @ -53,7 +53,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase): | |||||||
|             file.seek(0) |             file.seek(0) | ||||||
|             file_hash = sha512(file.read().encode()).hexdigest() |             file_hash = sha512(file.read().encode()).hexdigest() | ||||||
|             file.flush() |             file.flush() | ||||||
|             blueprints_discover()  # pylint: disable=no-value-for-parameter |             blueprints_discovery()  # pylint: disable=no-value-for-parameter | ||||||
|             instance = BlueprintInstance.objects.filter(name=blueprint_id).first() |             instance = BlueprintInstance.objects.filter(name=blueprint_id).first() | ||||||
|             self.assertEqual(instance.last_applied_hash, file_hash) |             self.assertEqual(instance.last_applied_hash, file_hash) | ||||||
|             self.assertEqual( |             self.assertEqual( | ||||||
| @ -81,7 +81,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase): | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             file.flush() |             file.flush() | ||||||
|             blueprints_discover()  # pylint: disable=no-value-for-parameter |             blueprints_discovery()  # pylint: disable=no-value-for-parameter | ||||||
|             blueprint = BlueprintInstance.objects.filter(name="foo").first() |             blueprint = BlueprintInstance.objects.filter(name="foo").first() | ||||||
|             self.assertEqual( |             self.assertEqual( | ||||||
|                 blueprint.last_applied_hash, |                 blueprint.last_applied_hash, | ||||||
| @ -106,7 +106,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase): | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             file.flush() |             file.flush() | ||||||
|             blueprints_discover()  # pylint: disable=no-value-for-parameter |             blueprints_discovery()  # pylint: disable=no-value-for-parameter | ||||||
|             blueprint.refresh_from_db() |             blueprint.refresh_from_db() | ||||||
|             self.assertEqual( |             self.assertEqual( | ||||||
|                 blueprint.last_applied_hash, |                 blueprint.last_applied_hash, | ||||||
|  | |||||||
| @ -40,6 +40,10 @@ from authentik.lib.models import SerializerModel | |||||||
| from authentik.outposts.models import OutpostServiceConnection | from authentik.outposts.models import OutpostServiceConnection | ||||||
| from authentik.policies.models import Policy, PolicyBindingModel | from authentik.policies.models import Policy, PolicyBindingModel | ||||||
|  |  | ||||||
|  | # Context set when the serializer is created in a blueprint context | ||||||
|  | # Update website/developer-docs/blueprints/v1/models.md when used | ||||||
|  | SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry" | ||||||
|  |  | ||||||
|  |  | ||||||
| def is_model_allowed(model: type[Model]) -> bool: | def is_model_allowed(model: type[Model]) -> bool: | ||||||
|     """Check if model is allowed""" |     """Check if model is allowed""" | ||||||
| @ -158,7 +162,12 @@ class Importer: | |||||||
|             raise EntryInvalidError(f"Model {model} not allowed") |             raise EntryInvalidError(f"Model {model} not allowed") | ||||||
|         if issubclass(model, BaseMetaModel): |         if issubclass(model, BaseMetaModel): | ||||||
|             serializer_class: type[Serializer] = model.serializer() |             serializer_class: type[Serializer] = model.serializer() | ||||||
|             serializer = serializer_class(data=entry.get_attrs(self.__import)) |             serializer = serializer_class( | ||||||
|  |                 data=entry.get_attrs(self.__import), | ||||||
|  |                 context={ | ||||||
|  |                     SERIALIZER_CONTEXT_BLUEPRINT: entry, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|             try: |             try: | ||||||
|                 serializer.is_valid(raise_exception=True) |                 serializer.is_valid(raise_exception=True) | ||||||
|             except ValidationError as exc: |             except ValidationError as exc: | ||||||
| @ -217,7 +226,12 @@ class Importer: | |||||||
|         always_merger.merge(full_data, updated_identifiers) |         always_merger.merge(full_data, updated_identifiers) | ||||||
|         serializer_kwargs["data"] = full_data |         serializer_kwargs["data"] = full_data | ||||||
|  |  | ||||||
|         serializer: Serializer = model().serializer(**serializer_kwargs) |         serializer: Serializer = model().serializer( | ||||||
|  |             context={ | ||||||
|  |                 SERIALIZER_CONTEXT_BLUEPRINT: entry, | ||||||
|  |             }, | ||||||
|  |             **serializer_kwargs, | ||||||
|  |         ) | ||||||
|         try: |         try: | ||||||
|             serializer.is_valid(raise_exception=True) |             serializer.is_valid(raise_exception=True) | ||||||
|         except ValidationError as exc: |         except ValidationError as exc: | ||||||
|  | |||||||
| @ -76,7 +76,7 @@ class BlueprintEventHandler(FileSystemEventHandler): | |||||||
|             return |             return | ||||||
|         if isinstance(event, FileCreatedEvent): |         if isinstance(event, FileCreatedEvent): | ||||||
|             LOGGER.debug("new blueprint file created, starting discovery") |             LOGGER.debug("new blueprint file created, starting discovery") | ||||||
|             blueprints_discover.delay() |             blueprints_discovery.delay() | ||||||
|         if isinstance(event, FileModifiedEvent): |         if isinstance(event, FileModifiedEvent): | ||||||
|             path = Path(event.src_path) |             path = Path(event.src_path) | ||||||
|             root = Path(CONFIG.y("blueprints_dir")).absolute() |             root = Path(CONFIG.y("blueprints_dir")).absolute() | ||||||
| @ -122,7 +122,7 @@ def blueprints_find(): | |||||||
|         ) |         ) | ||||||
|         blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None |         blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None | ||||||
|         blueprints.append(blueprint) |         blueprints.append(blueprint) | ||||||
|         LOGGER.info( |         LOGGER.debug( | ||||||
|             "parsed & loaded blueprint", |             "parsed & loaded blueprint", | ||||||
|             hash=file_hash, |             hash=file_hash, | ||||||
|             path=str(path), |             path=str(path), | ||||||
| @ -134,7 +134,7 @@ def blueprints_find(): | |||||||
|     throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True |     throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True | ||||||
| ) | ) | ||||||
| @prefill_task | @prefill_task | ||||||
| def blueprints_discover(self: MonitoredTask): | def blueprints_discovery(self: MonitoredTask): | ||||||
|     """Find blueprints and check if they need to be created in the database""" |     """Find blueprints and check if they need to be created in the database""" | ||||||
|     count = 0 |     count = 0 | ||||||
|     for blueprint in blueprints_find(): |     for blueprint in blueprints_find(): | ||||||
|  | |||||||
| @ -35,6 +35,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|         fields = [ |         fields = [ | ||||||
|             "pk", |             "pk", | ||||||
|             "name", |             "name", | ||||||
|  |             "authentication_flow", | ||||||
|             "authorization_flow", |             "authorization_flow", | ||||||
|             "property_mappings", |             "property_mappings", | ||||||
|             "component", |             "component", | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from authentik.api.authorization import OwnerSuperuserPermissions | from authentik.api.authorization import OwnerSuperuserPermissions | ||||||
| from authentik.api.decorators import permission_required | from authentik.api.decorators import permission_required | ||||||
| from authentik.blueprints.api import ManagedSerializer | from authentik.blueprints.api import ManagedSerializer | ||||||
|  | from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.users import UserSerializer | from authentik.core.api.users import UserSerializer | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| @ -29,6 +30,11 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): | |||||||
|  |  | ||||||
|     user_obj = UserSerializer(required=False, source="user", read_only=True) |     user_obj = UserSerializer(required=False, source="user", read_only=True) | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs) -> None: | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         if SERIALIZER_CONTEXT_BLUEPRINT in self.context: | ||||||
|  |             self.fields["key"] = CharField() | ||||||
|  |  | ||||||
|     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: |     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: | ||||||
|         """Ensure only API or App password tokens are created.""" |         """Ensure only API or App password tokens are created.""" | ||||||
|         request: Request = self.context.get("request") |         request: Request = self.context.get("request") | ||||||
|  | |||||||
| @ -67,11 +67,12 @@ from authentik.core.models import ( | |||||||
|     TokenIntents, |     TokenIntents, | ||||||
|     User, |     User, | ||||||
| ) | ) | ||||||
| from authentik.events.models import EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.exceptions import FlowNonApplicableException | from authentik.flows.exceptions import FlowNonApplicableException | ||||||
| from authentik.flows.models import FlowToken | from authentik.flows.models import FlowToken | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||||
| from authentik.flows.views.executor import QS_KEY_TOKEN | from authentik.flows.views.executor import QS_KEY_TOKEN | ||||||
|  | from authentik.lib.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 | ||||||
| @ -211,8 +212,9 @@ class UserMetricsSerializer(PassiveSerializer): | |||||||
|     def get_logins(self, _): |     def get_logins(self, _): | ||||||
|         """Get successful logins per 8 hours for the last 7 days""" |         """Get successful logins per 8 hours for the last 7 days""" | ||||||
|         user = self.context["user"] |         user = self.context["user"] | ||||||
|  |         request = self.context["request"] | ||||||
|         return ( |         return ( | ||||||
|             get_objects_for_user(user, "authentik_events.view_event").filter( |             get_objects_for_user(request.user, "authentik_events.view_event").filter( | ||||||
|                 action=EventAction.LOGIN, user__pk=user.pk |                 action=EventAction.LOGIN, user__pk=user.pk | ||||||
|             ) |             ) | ||||||
|             # 3 data points per day, so 8 hour spans |             # 3 data points per day, so 8 hour spans | ||||||
| @ -223,8 +225,9 @@ class UserMetricsSerializer(PassiveSerializer): | |||||||
|     def get_logins_failed(self, _): |     def get_logins_failed(self, _): | ||||||
|         """Get failed logins per 8 hours for the last 7 days""" |         """Get failed logins per 8 hours for the last 7 days""" | ||||||
|         user = self.context["user"] |         user = self.context["user"] | ||||||
|  |         request = self.context["request"] | ||||||
|         return ( |         return ( | ||||||
|             get_objects_for_user(user, "authentik_events.view_event").filter( |             get_objects_for_user(request.user, "authentik_events.view_event").filter( | ||||||
|                 action=EventAction.LOGIN_FAILED, context__username=user.username |                 action=EventAction.LOGIN_FAILED, context__username=user.username | ||||||
|             ) |             ) | ||||||
|             # 3 data points per day, so 8 hour spans |             # 3 data points per day, so 8 hour spans | ||||||
| @ -235,8 +238,9 @@ class UserMetricsSerializer(PassiveSerializer): | |||||||
|     def get_authorizations(self, _): |     def get_authorizations(self, _): | ||||||
|         """Get failed logins per 8 hours for the last 7 days""" |         """Get failed logins per 8 hours for the last 7 days""" | ||||||
|         user = self.context["user"] |         user = self.context["user"] | ||||||
|  |         request = self.context["request"] | ||||||
|         return ( |         return ( | ||||||
|             get_objects_for_user(user, "authentik_events.view_event").filter( |             get_objects_for_user(request.user, "authentik_events.view_event").filter( | ||||||
|                 action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk |                 action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk | ||||||
|             ) |             ) | ||||||
|             # 3 data points per day, so 8 hour spans |             # 3 data points per day, so 8 hour spans | ||||||
| @ -471,8 +475,9 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|     def metrics(self, request: Request, pk: int) -> Response: |     def metrics(self, request: Request, pk: int) -> Response: | ||||||
|         """User metrics per 1h""" |         """User metrics per 1h""" | ||||||
|         user: User = self.get_object() |         user: User = self.get_object() | ||||||
|         serializer = UserMetricsSerializer(True) |         serializer = UserMetricsSerializer(instance={}) | ||||||
|         serializer.context["user"] = user |         serializer.context["user"] = user | ||||||
|  |         serializer.context["request"] = request | ||||||
|         return Response(serializer.data) |         return Response(serializer.data) | ||||||
|  |  | ||||||
|     @permission_required("authentik_core.reset_user_password") |     @permission_required("authentik_core.reset_user_password") | ||||||
| @ -539,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|         send_mails(email_stage, message) |         send_mails(email_stage, message) | ||||||
|         return Response(status=204) |         return Response(status=204) | ||||||
|  |  | ||||||
|  |     @permission_required("authentik_core.impersonate") | ||||||
|  |     @extend_schema( | ||||||
|  |         request=OpenApiTypes.NONE, | ||||||
|  |         responses={ | ||||||
|  |             "204": OpenApiResponse(description="Successfully started impersonation"), | ||||||
|  |             "401": OpenApiResponse(description="Access denied"), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     @action(detail=True, methods=["POST"]) | ||||||
|  |     def impersonate(self, request: Request, pk: int) -> Response: | ||||||
|  |         """Impersonate a user""" | ||||||
|  |         if not CONFIG.y_bool("impersonation"): | ||||||
|  |             LOGGER.debug("User attempted to impersonate", user=request.user) | ||||||
|  |             return Response(status=401) | ||||||
|  |         if not request.user.has_perm("impersonate"): | ||||||
|  |             LOGGER.debug("User attempted to impersonate without permissions", user=request.user) | ||||||
|  |             return Response(status=401) | ||||||
|  |  | ||||||
|  |         user_to_be = self.get_object() | ||||||
|  |  | ||||||
|  |         request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user | ||||||
|  |         request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be | ||||||
|  |  | ||||||
|  |         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) | ||||||
|  |  | ||||||
|  |         return Response(status=201) | ||||||
|  |  | ||||||
|  |     @extend_schema( | ||||||
|  |         request=OpenApiTypes.NONE, | ||||||
|  |         responses={ | ||||||
|  |             "204": OpenApiResponse(description="Successfully started impersonation"), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     @action(detail=False, methods=["GET"]) | ||||||
|  |     def impersonate_end(self, request: Request) -> Response: | ||||||
|  |         """End Impersonation a user""" | ||||||
|  |         if ( | ||||||
|  |             SESSION_KEY_IMPERSONATE_USER not in request.session | ||||||
|  |             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session | ||||||
|  |         ): | ||||||
|  |             LOGGER.debug("Can't end impersonation", user=request.user) | ||||||
|  |             return Response(status=204) | ||||||
|  |  | ||||||
|  |         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] | ||||||
|  |  | ||||||
|  |         del request.session[SESSION_KEY_IMPERSONATE_USER] | ||||||
|  |         del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] | ||||||
|  |  | ||||||
|  |         Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user) | ||||||
|  |  | ||||||
|  |         return Response(status=204) | ||||||
|  |  | ||||||
|     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: |     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: | ||||||
|         """Custom filter_queryset method which ignores guardian, but still supports sorting""" |         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||||
|         for backend in list(self.filter_backends): |         for backend in list(self.filter_backends): | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ class AuthentikCoreConfig(ManagedAppConfig): | |||||||
|     label = "authentik_core" |     label = "authentik_core" | ||||||
|     verbose_name = "authentik Core" |     verbose_name = "authentik Core" | ||||||
|     mountpoint = "" |     mountpoint = "" | ||||||
|  |     ws_mountpoint = "authentik.core.urls" | ||||||
|     default = True |     default = True | ||||||
|  |  | ||||||
|     def reconcile_load_core_signals(self): |     def reconcile_load_core_signals(self): | ||||||
|  | |||||||
| @ -21,11 +21,14 @@ PROPERTY_MAPPING_TIME = Histogram( | |||||||
| class PropertyMappingEvaluator(BaseEvaluator): | class PropertyMappingEvaluator(BaseEvaluator): | ||||||
|     """Custom Evaluator that adds some different context variables.""" |     """Custom Evaluator that adds some different context variables.""" | ||||||
|  |  | ||||||
|  |     dry_run: bool | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         model: Model, |         model: Model, | ||||||
|         user: Optional[User] = None, |         user: Optional[User] = None, | ||||||
|         request: Optional[HttpRequest] = None, |         request: Optional[HttpRequest] = None, | ||||||
|  |         dry_run: Optional[bool] = False, | ||||||
|         **kwargs, |         **kwargs, | ||||||
|     ): |     ): | ||||||
|         if hasattr(model, "name"): |         if hasattr(model, "name"): | ||||||
| @ -42,9 +45,13 @@ class PropertyMappingEvaluator(BaseEvaluator): | |||||||
|             req.http_request = request |             req.http_request = request | ||||||
|         self._context["request"] = req |         self._context["request"] = req | ||||||
|         self._context.update(**kwargs) |         self._context.update(**kwargs) | ||||||
|  |         self.dry_run = dry_run | ||||||
|  |  | ||||||
|     def handle_error(self, exc: Exception, expression_source: str): |     def handle_error(self, exc: Exception, expression_source: str): | ||||||
|         """Exception Handler""" |         """Exception Handler""" | ||||||
|  |         # For dry-run requests we don't save exceptions | ||||||
|  |         if self.dry_run: | ||||||
|  |             return | ||||||
|         error_string = exception_to_string(exc) |         error_string = exception_to_string(exc) | ||||||
|         event = Event.new( |         event = Event.new( | ||||||
|             EventAction.PROPERTY_MAPPING_EXCEPTION, |             EventAction.PROPERTY_MAPPING_EXCEPTION, | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								authentik/core/migrations/0027_alter_user_uuid.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								authentik/core/migrations/0027_alter_user_uuid.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | # Generated by Django 4.1.7 on 2023-03-19 21:57 | ||||||
|  |  | ||||||
|  | import uuid | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0026_alter_propertymapping_name_alter_provider_name"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="user", | ||||||
|  |             name="uuid", | ||||||
|  |             field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | # Generated by Django 4.1.7 on 2023-03-23 21:44 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"), | ||||||
|  |         ("authentik_core", "0027_alter_user_uuid"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="provider", | ||||||
|  |             name="authentication_flow", | ||||||
|  |             field=models.ForeignKey( | ||||||
|  |                 help_text="Flow used for authentication when the associated application is accessed by an un-authenticated user.", | ||||||
|  |                 null=True, | ||||||
|  |                 on_delete=django.db.models.deletion.SET_NULL, | ||||||
|  |                 related_name="provider_authentication", | ||||||
|  |                 to="authentik_flows.flow", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -146,7 +146,7 @@ class UserManager(DjangoUserManager): | |||||||
| class User(SerializerModel, GuardianUserMixin, AbstractUser): | class User(SerializerModel, GuardianUserMixin, AbstractUser): | ||||||
|     """Custom User model to allow easier adding of user-based settings""" |     """Custom User model to allow easier adding of user-based settings""" | ||||||
|  |  | ||||||
|     uuid = models.UUIDField(default=uuid4, editable=False) |     uuid = models.UUIDField(default=uuid4, editable=False, unique=True) | ||||||
|     name = models.TextField(help_text=_("User's display name.")) |     name = models.TextField(help_text=_("User's display name.")) | ||||||
|     path = models.TextField(default="users") |     path = models.TextField(default="users") | ||||||
|  |  | ||||||
| @ -249,6 +249,17 @@ class Provider(SerializerModel): | |||||||
|  |  | ||||||
|     name = models.TextField(unique=True) |     name = models.TextField(unique=True) | ||||||
|  |  | ||||||
|  |     authentication_flow = models.ForeignKey( | ||||||
|  |         "authentik_flows.Flow", | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         help_text=_( | ||||||
|  |             "Flow used for authentication when the associated application is accessed by an " | ||||||
|  |             "un-authenticated user." | ||||||
|  |         ), | ||||||
|  |         related_name="provider_authentication", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     authorization_flow = models.ForeignKey( |     authorization_flow = models.ForeignKey( | ||||||
|         "authentik_flows.Flow", |         "authentik_flows.Flow", | ||||||
|         on_delete=models.CASCADE, |         on_delete=models.CASCADE, | ||||||
|  | |||||||
| @ -9,16 +9,13 @@ | |||||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||||
|         <title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title> |         <title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title> | ||||||
|         <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}"> |         <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}"> | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}"> |  | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}"> |  | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}"> |  | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}"> |  | ||||||
|         {% block head_before %} |         {% block head_before %} | ||||||
|         {% endblock %} |         {% endblock %} | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> | ||||||
|         <script src="{% static 'dist/poly.js' %}" type="module"></script> |         <script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script> | ||||||
|  |         <script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script> | ||||||
|         {% block head %} |         {% block head %} | ||||||
|         {% endblock %} |         {% endblock %} | ||||||
|         <meta name="sentry-trace" content="{{ sentry_trace }}" /> |         <meta name="sentry-trace" content="{{ sentry_trace }}" /> | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| {% extends "base/skeleton.html" %} | {% extends "base/skeleton.html" %} | ||||||
|  |  | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} |  | ||||||
|  |  | ||||||
| {% block head %} | {% block head %} | ||||||
| <script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script> | <script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script> | ||||||
| @ -15,19 +14,6 @@ | |||||||
| {% block body %} | {% block body %} | ||||||
| <ak-message-container></ak-message-container> | <ak-message-container></ak-message-container> | ||||||
| <ak-interface-admin> | <ak-interface-admin> | ||||||
|     <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> |     <ak-loading></ak-loading> | ||||||
|         <div class="pf-c-empty-state" style="height: 100vh;"> |  | ||||||
|             <div class="pf-c-empty-state__content"> |  | ||||||
|                 <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> |  | ||||||
|                     <span class="pf-c-spinner__clipper"></span> |  | ||||||
|                     <span class="pf-c-spinner__lead-ball"></span> |  | ||||||
|                     <span class="pf-c-spinner__tail-ball"></span> |  | ||||||
|                 </span> |  | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |  | ||||||
|                     {% trans "Loading..." %} |  | ||||||
|                 </h1> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </section> |  | ||||||
| </ak-interface-admin> | </ak-interface-admin> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| {% extends "base/skeleton.html" %} | {% extends "base/skeleton.html" %} | ||||||
|  |  | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} |  | ||||||
|  |  | ||||||
| {% block head_before %} | {% block head_before %} | ||||||
| {{ block.super }} | {{ block.super }} | ||||||
| @ -31,19 +30,6 @@ window.authentik.flow = { | |||||||
| {% block body %} | {% block body %} | ||||||
| <ak-message-container></ak-message-container> | <ak-message-container></ak-message-container> | ||||||
| <ak-flow-executor> | <ak-flow-executor> | ||||||
|     <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> |     <ak-loading></ak-loading> | ||||||
|         <div class="pf-c-empty-state" style="height: 100vh;"> |  | ||||||
|             <div class="pf-c-empty-state__content"> |  | ||||||
|                 <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> |  | ||||||
|                     <span class="pf-c-spinner__clipper"></span> |  | ||||||
|                     <span class="pf-c-spinner__lead-ball"></span> |  | ||||||
|                     <span class="pf-c-spinner__tail-ball"></span> |  | ||||||
|                 </span> |  | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |  | ||||||
|                     {% trans "Loading..." %} |  | ||||||
|                 </h1> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </section> |  | ||||||
| </ak-flow-executor> | </ak-flow-executor> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| {% extends "base/skeleton.html" %} | {% extends "base/skeleton.html" %} | ||||||
|  |  | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} |  | ||||||
|  |  | ||||||
| {% block head %} | {% block head %} | ||||||
| <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script> | <script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script> | ||||||
| @ -15,19 +14,6 @@ | |||||||
| {% block body %} | {% block body %} | ||||||
| <ak-message-container></ak-message-container> | <ak-message-container></ak-message-container> | ||||||
| <ak-interface-user> | <ak-interface-user> | ||||||
|     <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> |     <ak-loading></ak-loading> | ||||||
|         <div class="pf-c-empty-state" style="height: 100vh;"> |  | ||||||
|             <div class="pf-c-empty-state__content"> |  | ||||||
|                 <span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> |  | ||||||
|                     <span class="pf-c-spinner__clipper"></span> |  | ||||||
|                     <span class="pf-c-spinner__lead-ball"></span> |  | ||||||
|                     <span class="pf-c-spinner__tail-ball"></span> |  | ||||||
|                 </span> |  | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |  | ||||||
|                     {% trans "Loading..." %} |  | ||||||
|                 </h1> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </section> |  | ||||||
| </ak-interface-user> | </ak-interface-user> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | |||||||
| @ -43,14 +43,14 @@ class TestApplicationsAPI(APITestCase): | |||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.client.patch( |             self.client.patch( | ||||||
|                 reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}), |                 reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}), | ||||||
|                 {"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"}, |                 {"meta_launch_url": "https://%(username)s-test.test.goauthentik.io/%(username)s"}, | ||||||
|             ).status_code, |             ).status_code, | ||||||
|             200, |             200, | ||||||
|         ) |         ) | ||||||
|         self.allowed.refresh_from_db() |         self.allowed.refresh_from_db() | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.allowed.get_launch_url(self.user), |             self.allowed.get_launch_url(self.user), | ||||||
|             f"https://{self.user.username}.test.goauthentik.io/{self.user.username}", |             f"https://{self.user.username}-test.test.goauthentik.io/{self.user.username}", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def test_set_icon(self): |     def test_set_icon(self): | ||||||
| @ -129,6 +129,7 @@ class TestApplicationsAPI(APITestCase): | |||||||
|                         "provider_obj": { |                         "provider_obj": { | ||||||
|                             "assigned_application_name": "allowed", |                             "assigned_application_name": "allowed", | ||||||
|                             "assigned_application_slug": "allowed", |                             "assigned_application_slug": "allowed", | ||||||
|  |                             "authentication_flow": None, | ||||||
|                             "authorization_flow": str(self.provider.authorization_flow.pk), |                             "authorization_flow": str(self.provider.authorization_flow.pk), | ||||||
|                             "component": "ak-provider-oauth2-form", |                             "component": "ak-provider-oauth2-form", | ||||||
|                             "meta_model_name": "authentik_providers_oauth2.oauth2provider", |                             "meta_model_name": "authentik_providers_oauth2.oauth2provider", | ||||||
| @ -178,6 +179,7 @@ class TestApplicationsAPI(APITestCase): | |||||||
|                         "provider_obj": { |                         "provider_obj": { | ||||||
|                             "assigned_application_name": "allowed", |                             "assigned_application_name": "allowed", | ||||||
|                             "assigned_application_slug": "allowed", |                             "assigned_application_slug": "allowed", | ||||||
|  |                             "authentication_flow": None, | ||||||
|                             "authorization_flow": str(self.provider.authorization_flow.pk), |                             "authorization_flow": str(self.provider.authorization_flow.pk), | ||||||
|                             "component": "ak-provider-oauth2-form", |                             "component": "ak-provider-oauth2-form", | ||||||
|                             "meta_model_name": "authentik_providers_oauth2.oauth2provider", |                             "meta_model_name": "authentik_providers_oauth2.oauth2provider", | ||||||
|  | |||||||
| @ -1,14 +1,14 @@ | |||||||
| """impersonation tests""" | """impersonation tests""" | ||||||
| from json import loads | from json import loads | ||||||
|  |  | ||||||
| from django.test.testcases import TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | 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 | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestImpersonation(TestCase): | class TestImpersonation(APITestCase): | ||||||
|     """impersonation tests""" |     """impersonation tests""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
| @ -23,10 +23,10 @@ class TestImpersonation(TestCase): | |||||||
|         self.other_user.save() |         self.other_user.save() | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|         self.client.get( |         self.client.post( | ||||||
|             reverse( |             reverse( | ||||||
|                 "authentik_core:impersonate-init", |                 "authentik_api:user-impersonate", | ||||||
|                 kwargs={"user_id": self.other_user.pk}, |                 kwargs={"pk": self.other_user.pk}, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @ -35,7 +35,7 @@ class TestImpersonation(TestCase): | |||||||
|         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.user.username) |         self.assertEqual(response_body["original"]["username"], self.user.username) | ||||||
|  |  | ||||||
|         self.client.get(reverse("authentik_core:impersonate-end")) |         self.client.get(reverse("authentik_api:user-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()) | ||||||
| @ -46,9 +46,7 @@ class TestImpersonation(TestCase): | |||||||
|         """test impersonation without permissions""" |         """test impersonation without permissions""" | ||||||
|         self.client.force_login(self.other_user) |         self.client.force_login(self.other_user) | ||||||
|  |  | ||||||
|         self.client.get( |         self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.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")) | ||||||
|         response_body = loads(response.content.decode()) |         response_body = loads(response.content.decode()) | ||||||
| @ -58,5 +56,5 @@ class TestImpersonation(TestCase): | |||||||
|         """test un-impersonation without impersonating first""" |         """test un-impersonation without impersonating first""" | ||||||
|         self.client.force_login(self.other_user) |         self.client.force_login(self.other_user) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("authentik_core:impersonate-end")) |         response = self.client.get(reverse("authentik_api:user-impersonate-end")) | ||||||
|         self.assertRedirects(response, reverse("authentik_core:if-user")) |         self.assertEqual(response.status_code, 204) | ||||||
|  | |||||||
| @ -4,7 +4,10 @@ from guardian.shortcuts import get_anonymous_user | |||||||
|  |  | ||||||
| from authentik.core.exceptions import PropertyMappingExpressionException | from authentik.core.exceptions import PropertyMappingExpressionException | ||||||
| from authentik.core.models import PropertyMapping | from authentik.core.models import PropertyMapping | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.policies.expression.models import ExpressionPolicy | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestPropertyMappings(TestCase): | class TestPropertyMappings(TestCase): | ||||||
| @ -12,23 +15,24 @@ class TestPropertyMappings(TestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         super().setUp() |         super().setUp() | ||||||
|  |         self.user = create_test_admin_user() | ||||||
|         self.factory = RequestFactory() |         self.factory = RequestFactory() | ||||||
|  |  | ||||||
|     def test_expression(self): |     def test_expression(self): | ||||||
|         """Test expression""" |         """Test expression""" | ||||||
|         mapping = PropertyMapping.objects.create(name="test", expression="return 'test'") |         mapping = PropertyMapping.objects.create(name=generate_id(), expression="return 'test'") | ||||||
|         self.assertEqual(mapping.evaluate(None, None), "test") |         self.assertEqual(mapping.evaluate(None, None), "test") | ||||||
|  |  | ||||||
|     def test_expression_syntax(self): |     def test_expression_syntax(self): | ||||||
|         """Test expression syntax error""" |         """Test expression syntax error""" | ||||||
|         mapping = PropertyMapping.objects.create(name="test", expression="-") |         mapping = PropertyMapping.objects.create(name=generate_id(), expression="-") | ||||||
|         with self.assertRaises(PropertyMappingExpressionException): |         with self.assertRaises(PropertyMappingExpressionException): | ||||||
|             mapping.evaluate(None, None) |             mapping.evaluate(None, None) | ||||||
|  |  | ||||||
|     def test_expression_error_general(self): |     def test_expression_error_general(self): | ||||||
|         """Test expression error""" |         """Test expression error""" | ||||||
|         expr = "return aaa" |         expr = "return aaa" | ||||||
|         mapping = PropertyMapping.objects.create(name="test", expression=expr) |         mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr) | ||||||
|         with self.assertRaises(PropertyMappingExpressionException): |         with self.assertRaises(PropertyMappingExpressionException): | ||||||
|             mapping.evaluate(None, None) |             mapping.evaluate(None, None) | ||||||
|         events = Event.objects.filter( |         events = Event.objects.filter( | ||||||
| @ -41,7 +45,7 @@ class TestPropertyMappings(TestCase): | |||||||
|         """Test expression error (with user and http request""" |         """Test expression error (with user and http request""" | ||||||
|         expr = "return aaa" |         expr = "return aaa" | ||||||
|         request = self.factory.get("/") |         request = self.factory.get("/") | ||||||
|         mapping = PropertyMapping.objects.create(name="test", expression=expr) |         mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr) | ||||||
|         with self.assertRaises(PropertyMappingExpressionException): |         with self.assertRaises(PropertyMappingExpressionException): | ||||||
|             mapping.evaluate(get_anonymous_user(), request) |             mapping.evaluate(get_anonymous_user(), request) | ||||||
|         events = Event.objects.filter( |         events = Event.objects.filter( | ||||||
| @ -52,3 +56,23 @@ class TestPropertyMappings(TestCase): | |||||||
|         event = events.first() |         event = events.first() | ||||||
|         self.assertEqual(event.user["username"], "AnonymousUser") |         self.assertEqual(event.user["username"], "AnonymousUser") | ||||||
|         self.assertEqual(event.client_ip, "127.0.0.1") |         self.assertEqual(event.client_ip, "127.0.0.1") | ||||||
|  |  | ||||||
|  |     def test_call_policy(self): | ||||||
|  |         """test ak_call_policy""" | ||||||
|  |         expr = ExpressionPolicy.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             execution_logging=True, | ||||||
|  |             expression="return request.http_request.path", | ||||||
|  |         ) | ||||||
|  |         http_request = self.factory.get("/") | ||||||
|  |         tmpl = ( | ||||||
|  |             """ | ||||||
|  |         res = ak_call_policy('%s') | ||||||
|  |         result = [request.http_request.path, res.raw_result] | ||||||
|  |         return result | ||||||
|  |         """ | ||||||
|  |             % expr.name | ||||||
|  |         ) | ||||||
|  |         evaluator = PropertyMapping(expression=tmpl, name=generate_id()) | ||||||
|  |         res = evaluator.evaluate(self.user, http_request) | ||||||
|  |         self.assertEqual(res, ["/", "/"]) | ||||||
|  | |||||||
| @ -27,6 +27,6 @@ class UserSettingSerializer(PassiveSerializer): | |||||||
|  |  | ||||||
|     object_uid = CharField() |     object_uid = CharField() | ||||||
|     component = CharField() |     component = CharField() | ||||||
|     title = CharField() |     title = CharField(required=True) | ||||||
|     configure_url = CharField(required=False) |     configure_url = CharField(required=False) | ||||||
|     icon_url = CharField(required=False) |     icon_url = CharField(required=False) | ||||||
|  | |||||||
| @ -1,14 +1,18 @@ | |||||||
| """authentik URL Configuration""" | """authentik URL Configuration""" | ||||||
|  | from channels.auth import AuthMiddleware | ||||||
|  | from channels.sessions import CookieMiddleware | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.decorators import login_required | from django.contrib.auth.decorators import login_required | ||||||
| from django.urls import path | from django.urls import path | ||||||
| from django.views.decorators.csrf import ensure_csrf_cookie | from django.views.decorators.csrf import ensure_csrf_cookie | ||||||
| from django.views.generic import RedirectView | from django.views.generic import RedirectView | ||||||
|  |  | ||||||
| from authentik.core.views import apps, impersonate | from authentik.core.views import apps | ||||||
| from authentik.core.views.debug import AccessDeniedView | from authentik.core.views.debug import AccessDeniedView | ||||||
| from authentik.core.views.interface import FlowInterfaceView, InterfaceView | from authentik.core.views.interface import FlowInterfaceView, InterfaceView | ||||||
| from authentik.core.views.session import EndSessionView | from authentik.core.views.session import EndSessionView | ||||||
|  | from authentik.root.asgi_middleware import SessionMiddleware | ||||||
|  | from authentik.root.messages.consumer import MessageConsumer | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path( |     path( | ||||||
| @ -24,17 +28,6 @@ urlpatterns = [ | |||||||
|         apps.RedirectToAppLaunch.as_view(), |         apps.RedirectToAppLaunch.as_view(), | ||||||
|         name="application-launch", |         name="application-launch", | ||||||
|     ), |     ), | ||||||
|     # Impersonation |  | ||||||
|     path( |  | ||||||
|         "-/impersonation/<int:user_id>/", |  | ||||||
|         impersonate.ImpersonateInitView.as_view(), |  | ||||||
|         name="impersonate-init", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "-/impersonation/end/", |  | ||||||
|         impersonate.ImpersonateEndView.as_view(), |  | ||||||
|         name="impersonate-end", |  | ||||||
|     ), |  | ||||||
|     # Interfaces |     # Interfaces | ||||||
|     path( |     path( | ||||||
|         "if/admin/", |         "if/admin/", | ||||||
| @ -64,6 +57,12 @@ urlpatterns = [ | |||||||
|     ), |     ), | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | websocket_urlpatterns = [ | ||||||
|  |     path( | ||||||
|  |         "ws/client/", CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi()))) | ||||||
|  |     ), | ||||||
|  | ] | ||||||
|  |  | ||||||
| if settings.DEBUG: | if settings.DEBUG: | ||||||
|     urlpatterns += [ |     urlpatterns += [ | ||||||
|         path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"), |         path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"), | ||||||
|  | |||||||
| @ -12,16 +12,19 @@ from authentik.flows.challenge import ( | |||||||
|     RedirectChallenge, |     RedirectChallenge, | ||||||
| ) | ) | ||||||
| from authentik.flows.exceptions import FlowNonApplicableException | from authentik.flows.exceptions import FlowNonApplicableException | ||||||
| from authentik.flows.models import in_memory_stage | from authentik.flows.models import FlowDesignation, in_memory_stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner | from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner | ||||||
| from authentik.flows.stage import ChallengeStageView | from authentik.flows.stage import ChallengeStageView | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import ( | ||||||
|  |     SESSION_KEY_APPLICATION_PRE, | ||||||
|  |     SESSION_KEY_PLAN, | ||||||
|  |     ToDefaultFlow, | ||||||
|  | ) | ||||||
| from authentik.lib.utils.urls import redirect_with_qs | from authentik.lib.utils.urls import redirect_with_qs | ||||||
| from authentik.stages.consent.stage import ( | from authentik.stages.consent.stage import ( | ||||||
|     PLAN_CONTEXT_CONSENT_HEADER, |     PLAN_CONTEXT_CONSENT_HEADER, | ||||||
|     PLAN_CONTEXT_CONSENT_PERMISSIONS, |     PLAN_CONTEXT_CONSENT_PERMISSIONS, | ||||||
| ) | ) | ||||||
| from authentik.tenants.models import Tenant |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class RedirectToAppLaunch(View): | class RedirectToAppLaunch(View): | ||||||
| @ -36,10 +39,10 @@ class RedirectToAppLaunch(View): | |||||||
|         # Check if we're authenticated already, saves us the flow run |         # Check if we're authenticated already, saves us the flow run | ||||||
|         if request.user.is_authenticated: |         if request.user.is_authenticated: | ||||||
|             return HttpResponseRedirect(app.get_launch_url(request.user)) |             return HttpResponseRedirect(app.get_launch_url(request.user)) | ||||||
|  |         self.request.session[SESSION_KEY_APPLICATION_PRE] = app | ||||||
|         # otherwise, do a custom flow plan that includes the application that's |         # otherwise, do a custom flow plan that includes the application that's | ||||||
|         # being accessed, to improve usability |         # being accessed, to improve usability | ||||||
|         tenant: Tenant = request.tenant |         flow = ToDefaultFlow(request=request, designation=FlowDesignation.AUTHENTICATION).get_flow() | ||||||
|         flow = tenant.flow_authentication |  | ||||||
|         planner = FlowPlanner(flow) |         planner = FlowPlanner(flow) | ||||||
|         planner.allow_empty_flows = True |         planner.allow_empty_flows = True | ||||||
|         try: |         try: | ||||||
|  | |||||||
| @ -1,60 +0,0 @@ | |||||||
| """authentik impersonation views""" |  | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse |  | ||||||
| from django.shortcuts import get_object_or_404, redirect |  | ||||||
| from django.views import View |  | ||||||
| from structlog.stdlib import get_logger |  | ||||||
|  |  | ||||||
| from authentik.core.middleware import ( |  | ||||||
|     SESSION_KEY_IMPERSONATE_ORIGINAL_USER, |  | ||||||
|     SESSION_KEY_IMPERSONATE_USER, |  | ||||||
| ) |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.events.models import Event, EventAction |  | ||||||
| from authentik.lib.config import CONFIG |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ImpersonateInitView(View): |  | ||||||
|     """Initiate Impersonation""" |  | ||||||
|  |  | ||||||
|     def get(self, request: HttpRequest, user_id: int) -> HttpResponse: |  | ||||||
|         """Impersonation handler, checks permissions""" |  | ||||||
|         if not CONFIG.y_bool("impersonation"): |  | ||||||
|             LOGGER.debug("User attempted to impersonate", user=request.user) |  | ||||||
|             return HttpResponse("Unauthorized", status=401) |  | ||||||
|         if not request.user.has_perm("impersonate"): |  | ||||||
|             LOGGER.debug("User attempted to impersonate without permissions", user=request.user) |  | ||||||
|             return HttpResponse("Unauthorized", status=401) |  | ||||||
|  |  | ||||||
|         user_to_be = get_object_or_404(User, pk=user_id) |  | ||||||
|  |  | ||||||
|         request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user |  | ||||||
|         request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be |  | ||||||
|  |  | ||||||
|         Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be) |  | ||||||
|  |  | ||||||
|         return redirect("authentik_core:if-user") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ImpersonateEndView(View): |  | ||||||
|     """End User impersonation""" |  | ||||||
|  |  | ||||||
|     def get(self, request: HttpRequest) -> HttpResponse: |  | ||||||
|         """End Impersonation handler""" |  | ||||||
|         if ( |  | ||||||
|             SESSION_KEY_IMPERSONATE_USER not in request.session |  | ||||||
|             or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session |  | ||||||
|         ): |  | ||||||
|             LOGGER.debug("Can't end impersonation", user=request.user) |  | ||||||
|             return redirect("authentik_core:if-user") |  | ||||||
|  |  | ||||||
|         original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] |  | ||||||
|  |  | ||||||
|         del request.session[SESSION_KEY_IMPERSONATE_USER] |  | ||||||
|         del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] |  | ||||||
|  |  | ||||||
|         Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user) |  | ||||||
|  |  | ||||||
|         return redirect("authentik_core:root-redirect") |  | ||||||
| @ -214,11 +214,18 @@ class Event(SerializerModel, ExpiringModel): | |||||||
|         Events independently from requests. |         Events independently from requests. | ||||||
|         `user` arguments optionally overrides user from requests.""" |         `user` arguments optionally overrides user from requests.""" | ||||||
|         if request: |         if request: | ||||||
|  |             from authentik.flows.views.executor import QS_QUERY | ||||||
|  |  | ||||||
|             self.context["http_request"] = { |             self.context["http_request"] = { | ||||||
|                 "path": request.path, |                 "path": request.path, | ||||||
|                 "method": request.method, |                 "method": request.method, | ||||||
|                 "args": QueryDict(request.META.get("QUERY_STRING", "")), |                 "args": QueryDict(request.META.get("QUERY_STRING", "")), | ||||||
|             } |             } | ||||||
|  |             # Special case for events created during flow execution | ||||||
|  |             # since they keep the http query within a wrapped query | ||||||
|  |             if QS_QUERY in self.context["http_request"]["args"]: | ||||||
|  |                 wrapped = self.context["http_request"]["args"][QS_QUERY] | ||||||
|  |                 self.context["http_request"]["args"] = QueryDict(wrapped) | ||||||
|         if hasattr(request, "tenant"): |         if hasattr(request, "tenant"): | ||||||
|             tenant: Tenant = request.tenant |             tenant: Tenant = request.tenant | ||||||
|             # Because self.created only gets set on save, we can't use it's value here |             # Because self.created only gets set on save, we can't use it's value here | ||||||
|  | |||||||
| @ -41,7 +41,7 @@ class TaskResult: | |||||||
|  |  | ||||||
|     def with_error(self, exc: Exception) -> "TaskResult": |     def with_error(self, exc: Exception) -> "TaskResult": | ||||||
|         """Since errors might not always be pickle-able, set the traceback""" |         """Since errors might not always be pickle-able, set the traceback""" | ||||||
|         self.messages.append(str(exc)) |         self.messages.append(exception_to_string(exc)) | ||||||
|         return self |         return self | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -23,7 +23,8 @@ class DiagramElement: | |||||||
|     style: list[str] = field(default_factory=lambda: ["[", "]"]) |     style: list[str] = field(default_factory=lambda: ["[", "]"]) | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}' |         description = self.description.replace('"', "#quot;") | ||||||
|  |         element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}' | ||||||
|         if self.action is not None: |         if self.action is not None: | ||||||
|             if self.action != "": |             if self.action != "": | ||||||
|                 element = f"--{self.action}--> {element}" |                 element = f"--{self.action}--> {element}" | ||||||
|  | |||||||
| @ -271,6 +271,15 @@ class ConfigurableStage(models.Model): | |||||||
|         abstract = True |         abstract = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FriendlyNamedStage(models.Model): | ||||||
|  |     """Abstract base class for a Stage that can have a user friendly name configured.""" | ||||||
|  |  | ||||||
|  |     friendly_name = models.TextField(null=True) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         abstract = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class FlowToken(Token): | class FlowToken(Token): | ||||||
|     """Subclass of a standard Token, stores the currently active flow plan upon creation. |     """Subclass of a standard Token, stores the currently active flow plan upon creation. | ||||||
|     Can be used to later resume a flow.""" |     Can be used to later resume a flow.""" | ||||||
|  | |||||||
| @ -204,12 +204,12 @@ class ChallengeStageView(StageView): | |||||||
|         for field, errors in response.errors.items(): |         for field, errors in response.errors.items(): | ||||||
|             for error in errors: |             for error in errors: | ||||||
|                 full_errors.setdefault(field, []) |                 full_errors.setdefault(field, []) | ||||||
|                 full_errors[field].append( |                 field_error = { | ||||||
|                     { |                     "string": str(error), | ||||||
|                         "string": str(error), |                 } | ||||||
|                         "code": error.code, |                 if hasattr(error, "code"): | ||||||
|                     } |                     field_error["code"] = error.code | ||||||
|                 ) |                 full_errors[field].append(field_error) | ||||||
|         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(): | ||||||
|             self.logger.error( |             self.logger.error( | ||||||
|  | |||||||
| @ -2,10 +2,13 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_flow | 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_APPLICATION_PRE, SESSION_KEY_PLAN | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from authentik.providers.oauth2.models import OAuth2Provider | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestHelperView(TestCase): | class TestHelperView(TestCase): | ||||||
| @ -22,6 +25,41 @@ class TestHelperView(TestCase): | |||||||
|         self.assertEqual(response.status_code, 302) |         self.assertEqual(response.status_code, 302) | ||||||
|         self.assertEqual(response.url, expected_url) |         self.assertEqual(response.url, expected_url) | ||||||
|  |  | ||||||
|  |     def test_default_view_app(self): | ||||||
|  |         """Test that ToDefaultFlow returns the expected URL (when accessing an application)""" | ||||||
|  |         Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete() | ||||||
|  |         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||||
|  |         self.client.session[SESSION_KEY_APPLICATION_PRE] = Application( | ||||||
|  |             name=generate_id(), | ||||||
|  |             slug=generate_id(), | ||||||
|  |             provider=OAuth2Provider( | ||||||
|  |                 name=generate_id(), | ||||||
|  |                 authentication_flow=flow, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_flows:default-authentication"), | ||||||
|  |         ) | ||||||
|  |         expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||||
|  |         self.assertEqual(response.status_code, 302) | ||||||
|  |         self.assertEqual(response.url, expected_url) | ||||||
|  |  | ||||||
|  |     def test_default_view_app_no_provider(self): | ||||||
|  |         """Test that ToDefaultFlow returns the expected URL | ||||||
|  |         (when accessing an application, without a provider)""" | ||||||
|  |         Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete() | ||||||
|  |         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||||
|  |         self.client.session[SESSION_KEY_APPLICATION_PRE] = Application( | ||||||
|  |             name=generate_id(), | ||||||
|  |             slug=generate_id(), | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_flows:default-authentication"), | ||||||
|  |         ) | ||||||
|  |         expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||||
|  |         self.assertEqual(response.status_code, 302) | ||||||
|  |         self.assertEqual(response.url, expected_url) | ||||||
|  |  | ||||||
|     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.objects.filter(designation=FlowDesignation.INVALIDATION).delete() |         Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete() | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ from sentry_sdk.api import set_tag | |||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application | ||||||
| from authentik.events.models import Event, EventAction, cleanse_dict | from authentik.events.models import Event, EventAction, cleanse_dict | ||||||
| from authentik.flows.challenge import ( | from authentik.flows.challenge import ( | ||||||
|     Challenge, |     Challenge, | ||||||
| @ -68,6 +69,7 @@ SESSION_KEY_GET = "authentik/flows/get" | |||||||
| SESSION_KEY_POST = "authentik/flows/post" | SESSION_KEY_POST = "authentik/flows/post" | ||||||
| SESSION_KEY_HISTORY = "authentik/flows/history" | SESSION_KEY_HISTORY = "authentik/flows/history" | ||||||
| QS_KEY_TOKEN = "flow_token"  # nosec | QS_KEY_TOKEN = "flow_token"  # nosec | ||||||
|  | QS_QUERY = "query" | ||||||
|  |  | ||||||
|  |  | ||||||
| def challenge_types(): | def challenge_types(): | ||||||
| @ -172,7 +174,7 @@ class FlowExecutorView(APIView): | |||||||
|             op="authentik.flow.executor.dispatch", description=self.flow.slug |             op="authentik.flow.executor.dispatch", description=self.flow.slug | ||||||
|         ) as span: |         ) as span: | ||||||
|             span.set_data("authentik Flow", self.flow.slug) |             span.set_data("authentik Flow", self.flow.slug) | ||||||
|             get_params = QueryDict(request.GET.get("query", "")) |             get_params = QueryDict(request.GET.get(QS_QUERY, "")) | ||||||
|             if QS_KEY_TOKEN in get_params: |             if QS_KEY_TOKEN in get_params: | ||||||
|                 plan = self._check_flow_token(get_params[QS_KEY_TOKEN]) |                 plan = self._check_flow_token(get_params[QS_KEY_TOKEN]) | ||||||
|                 if plan: |                 if plan: | ||||||
| @ -475,20 +477,32 @@ class ToDefaultFlow(View): | |||||||
|         LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter) |         LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter) | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: |     def get_flow(self) -> Flow: | ||||||
|         tenant: Tenant = request.tenant |         """Get a flow for the selected designation""" | ||||||
|  |         tenant: Tenant = self.request.tenant | ||||||
|         flow = None |         flow = None | ||||||
|         # First, attempt to get default flow from tenant |         # First, attempt to get default flow from tenant | ||||||
|         if self.designation == FlowDesignation.AUTHENTICATION: |         if self.designation == FlowDesignation.AUTHENTICATION: | ||||||
|             flow = tenant.flow_authentication |             flow = tenant.flow_authentication | ||||||
|         if self.designation == FlowDesignation.INVALIDATION: |             # Check if we have a default flow from application | ||||||
|  |             application: Optional[Application] = self.request.session.get( | ||||||
|  |                 SESSION_KEY_APPLICATION_PRE | ||||||
|  |             ) | ||||||
|  |             if application and application.provider and application.provider.authentication_flow: | ||||||
|  |                 flow = application.provider.authentication_flow | ||||||
|  |         elif self.designation == FlowDesignation.INVALIDATION: | ||||||
|             flow = tenant.flow_invalidation |             flow = tenant.flow_invalidation | ||||||
|  |         if flow: | ||||||
|  |             return flow | ||||||
|         # If no flow was set, get the first based on slug and policy |         # If no flow was set, get the first based on slug and policy | ||||||
|         if not flow: |         flow = self.flow_by_policy(self.request, designation=self.designation) | ||||||
|             flow = self.flow_by_policy(request, designation=self.designation) |         if flow: | ||||||
|  |             return flow | ||||||
|         # If we still don't have a flow, 404 |         # If we still don't have a flow, 404 | ||||||
|         if not flow: |         raise Http404 | ||||||
|             raise Http404 |  | ||||||
|  |     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||||
|  |         flow = self.get_flow() | ||||||
|         # If user already has a pending plan, clear it so we don't have to later. |         # If user already has a pending plan, clear it so we don't have to later. | ||||||
|         if SESSION_KEY_PLAN in self.request.session: |         if SESSION_KEY_PLAN in self.request.session: | ||||||
|             plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] |             plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ from typing import Any, Iterable, Optional | |||||||
| from cachetools import TLRUCache, cached | from cachetools import TLRUCache, cached | ||||||
| from django.core.exceptions import FieldError | from django.core.exceptions import FieldError | ||||||
| from django_otp import devices_for_user | from django_otp import devices_for_user | ||||||
|  | from guardian.shortcuts import get_anonymous_user | ||||||
| from rest_framework.serializers import ValidationError | from rest_framework.serializers import ValidationError | ||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from sentry_sdk.tracing import Span | from sentry_sdk.tracing import Span | ||||||
| @ -16,7 +17,9 @@ from structlog.stdlib import get_logger | |||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.events.models import Event | from authentik.events.models import Event | ||||||
| from authentik.lib.utils.http import get_http_session | from authentik.lib.utils.http import get_http_session | ||||||
| from authentik.policies.types import PolicyRequest | from authentik.policies.models import Policy, PolicyBinding | ||||||
|  | from authentik.policies.process import PolicyProcess | ||||||
|  | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| @ -37,19 +40,20 @@ class BaseEvaluator: | |||||||
|         # update website/docs/expressions/_objects.md |         # update website/docs/expressions/_objects.md | ||||||
|         # update website/docs/expressions/_functions.md |         # update website/docs/expressions/_functions.md | ||||||
|         self._globals = { |         self._globals = { | ||||||
|             "regex_match": BaseEvaluator.expr_regex_match, |             "ak_call_policy": self.expr_func_call_policy, | ||||||
|             "regex_replace": BaseEvaluator.expr_regex_replace, |             "ak_create_event": self.expr_event_create, | ||||||
|             "list_flatten": BaseEvaluator.expr_flatten, |  | ||||||
|             "ak_is_group_member": BaseEvaluator.expr_is_group_member, |             "ak_is_group_member": BaseEvaluator.expr_is_group_member, | ||||||
|  |             "ak_logger": get_logger(self._filename).bind(), | ||||||
|             "ak_user_by": BaseEvaluator.expr_user_by, |             "ak_user_by": BaseEvaluator.expr_user_by, | ||||||
|             "ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator, |             "ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator, | ||||||
|             "resolve_dns": BaseEvaluator.expr_resolve_dns, |  | ||||||
|             "reverse_dns": BaseEvaluator.expr_reverse_dns, |  | ||||||
|             "ak_create_event": self.expr_event_create, |  | ||||||
|             "ak_logger": get_logger(self._filename).bind(), |  | ||||||
|             "requests": get_http_session(), |  | ||||||
|             "ip_address": ip_address, |             "ip_address": ip_address, | ||||||
|             "ip_network": ip_network, |             "ip_network": ip_network, | ||||||
|  |             "list_flatten": BaseEvaluator.expr_flatten, | ||||||
|  |             "regex_match": BaseEvaluator.expr_regex_match, | ||||||
|  |             "regex_replace": BaseEvaluator.expr_regex_replace, | ||||||
|  |             "requests": get_http_session(), | ||||||
|  |             "resolve_dns": BaseEvaluator.expr_resolve_dns, | ||||||
|  |             "reverse_dns": BaseEvaluator.expr_reverse_dns, | ||||||
|         } |         } | ||||||
|         self._context = {} |         self._context = {} | ||||||
|  |  | ||||||
| @ -152,6 +156,19 @@ class BaseEvaluator: | |||||||
|                 return |                 return | ||||||
|         event.save() |         event.save() | ||||||
|  |  | ||||||
|  |     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.") | ||||||
|  |         user = self._context.get("user", get_anonymous_user()) | ||||||
|  |         req = PolicyRequest(user) | ||||||
|  |         if "request" in self._context: | ||||||
|  |             req = self._context["request"] | ||||||
|  |         req.context.update(kwargs) | ||||||
|  |         proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None) | ||||||
|  |         return proc.profiling_wrapper() | ||||||
|  |  | ||||||
|     def wrap_expression(self, expression: str, params: Iterable[str]) -> str: |     def wrap_expression(self, expression: str, params: Iterable[str]) -> str: | ||||||
|         """Wrap expression in a function, call it, and save the result as `result`""" |         """Wrap expression in a function, call it, and save the result as `result`""" | ||||||
|         handler_signature = ",".join(params) |         handler_signature = ",".join(params) | ||||||
|  | |||||||
| @ -81,7 +81,8 @@ class DomainlessFormattedURLValidator(DomainlessURLValidator): | |||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self.host_re = r"([%\(\)a-zA-Z])+" + self.domain_re + self.domain_re |         self.formatter_re = r"([%\(\)a-zA-Z])*" | ||||||
|  |         self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)" | ||||||
|         self.regex = _lazy_re_compile( |         self.regex = _lazy_re_compile( | ||||||
|             r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately |             r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately | ||||||
|             r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication |             r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication | ||||||
|  | |||||||
| @ -19,9 +19,12 @@ from rest_framework.exceptions import APIException | |||||||
| from sentry_sdk import HttpTransport | from sentry_sdk import HttpTransport | ||||||
| from sentry_sdk import init as sentry_sdk_init | from sentry_sdk import init as sentry_sdk_init | ||||||
| from sentry_sdk.api import set_tag | from sentry_sdk.api import set_tag | ||||||
|  | from sentry_sdk.integrations.argv import ArgvIntegration | ||||||
| from sentry_sdk.integrations.celery import CeleryIntegration | from sentry_sdk.integrations.celery import CeleryIntegration | ||||||
| from sentry_sdk.integrations.django import DjangoIntegration | from sentry_sdk.integrations.django import DjangoIntegration | ||||||
| from sentry_sdk.integrations.redis import RedisIntegration | from sentry_sdk.integrations.redis import RedisIntegration | ||||||
|  | from sentry_sdk.integrations.socket import SocketIntegration | ||||||
|  | from sentry_sdk.integrations.stdlib import StdlibIntegration | ||||||
| from sentry_sdk.integrations.threading import ThreadingIntegration | from sentry_sdk.integrations.threading import ThreadingIntegration | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| from websockets.exceptions import WebSocketException | from websockets.exceptions import WebSocketException | ||||||
| @ -61,10 +64,13 @@ def sentry_init(**sentry_init_kwargs): | |||||||
|     sentry_sdk_init( |     sentry_sdk_init( | ||||||
|         dsn=CONFIG.y("error_reporting.sentry_dsn"), |         dsn=CONFIG.y("error_reporting.sentry_dsn"), | ||||||
|         integrations=[ |         integrations=[ | ||||||
|  |             ArgvIntegration(), | ||||||
|  |             StdlibIntegration(), | ||||||
|             DjangoIntegration(transaction_style="function_name"), |             DjangoIntegration(transaction_style="function_name"), | ||||||
|             CeleryIntegration(), |             CeleryIntegration(monitor_beat_tasks=True), | ||||||
|             RedisIntegration(), |             RedisIntegration(), | ||||||
|             ThreadingIntegration(propagate_hub=True), |             ThreadingIntegration(propagate_hub=True), | ||||||
|  |             SocketIntegration(), | ||||||
|         ], |         ], | ||||||
|         before_send=before_send, |         before_send=before_send, | ||||||
|         traces_sampler=traces_sampler, |         traces_sampler=traces_sampler, | ||||||
|  | |||||||
| @ -1,4 +1,7 @@ | |||||||
| """Test utils""" | """Test utils""" | ||||||
|  | from inspect import currentframe | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
| from django.contrib.messages.middleware import MessageMiddleware | from django.contrib.messages.middleware import MessageMiddleware | ||||||
| from django.contrib.sessions.middleware import SessionMiddleware | from django.contrib.sessions.middleware import SessionMiddleware | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| @ -11,6 +14,21 @@ def dummy_get_response(request: HttpRequest):  # pragma: no cover | |||||||
|     return None |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_fixture(path: str, **kwargs) -> str: | ||||||
|  |     """Load fixture, optionally formatting it with kwargs""" | ||||||
|  |     current = currentframe() | ||||||
|  |     parent = current.f_back | ||||||
|  |     calling_file_path = parent.f_globals["__file__"] | ||||||
|  |     with open( | ||||||
|  |         Path(calling_file_path).resolve().parent / Path(path), "r", encoding="utf-8" | ||||||
|  |     ) as _fixture: | ||||||
|  |         fixture = _fixture.read() | ||||||
|  |         try: | ||||||
|  |             return fixture % kwargs | ||||||
|  |         except TypeError: | ||||||
|  |             return fixture | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_request(*args, user=None, **kwargs): | def get_request(*args, user=None, **kwargs): | ||||||
|     """Get a request with usable session""" |     """Get a request with usable session""" | ||||||
|     request = RequestFactory().get(*args, **kwargs) |     request = RequestFactory().get(*args, **kwargs) | ||||||
|  | |||||||
| @ -38,13 +38,17 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]: | |||||||
|     if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META: |     if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META: | ||||||
|         return None |         return None | ||||||
|     fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER] |     fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER] | ||||||
|     tokens = Token.filter_not_expired( |     token = ( | ||||||
|         key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API |         Token.filter_not_expired( | ||||||
|  |             key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API | ||||||
|  |         ) | ||||||
|  |         .select_related("user") | ||||||
|  |         .first() | ||||||
|     ) |     ) | ||||||
|     if not tokens.exists(): |     if not token: | ||||||
|         LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip) |         LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip) | ||||||
|         return None |         return None | ||||||
|     user = tokens.first().user |     user = token.user | ||||||
|     if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): |     if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): | ||||||
|         LOGGER.warning( |         LOGGER.warning( | ||||||
|             "Remote-IP override: user doesn't have permission", |             "Remote-IP override: user doesn't have permission", | ||||||
|  | |||||||
| @ -28,6 +28,7 @@ from authentik.outposts.models import ( | |||||||
| ) | ) | ||||||
| from authentik.providers.ldap.models import LDAPProvider | from authentik.providers.ldap.models import LDAPProvider | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
|  | from authentik.providers.radius.models import RadiusProvider | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostSerializer(ModelSerializer): | class OutpostSerializer(ModelSerializer): | ||||||
| @ -51,6 +52,7 @@ class OutpostSerializer(ModelSerializer): | |||||||
|         type_map = { |         type_map = { | ||||||
|             OutpostType.LDAP: LDAPProvider, |             OutpostType.LDAP: LDAPProvider, | ||||||
|             OutpostType.PROXY: ProxyProvider, |             OutpostType.PROXY: ProxyProvider, | ||||||
|  |             OutpostType.RADIUS: RadiusProvider, | ||||||
|             None: Provider, |             None: Provider, | ||||||
|         } |         } | ||||||
|         for provider in providers: |         for provider in providers: | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ class AuthentikOutpostConfig(ManagedAppConfig): | |||||||
|     label = "authentik_outposts" |     label = "authentik_outposts" | ||||||
|     verbose_name = "authentik Outpost" |     verbose_name = "authentik Outpost" | ||||||
|     default = True |     default = True | ||||||
|  |     ws_mountpoint = "authentik.outposts.urls" | ||||||
|  |  | ||||||
|     def reconcile_load_outposts_signals(self): |     def reconcile_load_outposts_signals(self): | ||||||
|         """Load outposts signals""" |         """Load outposts signals""" | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ from typing import TYPE_CHECKING | |||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from kubernetes.client import ( | from kubernetes.client import ( | ||||||
|     AppsV1Api, |     AppsV1Api, | ||||||
|  |     V1Capabilities, | ||||||
|     V1Container, |     V1Container, | ||||||
|     V1ContainerPort, |     V1ContainerPort, | ||||||
|     V1Deployment, |     V1Deployment, | ||||||
| @ -13,9 +14,12 @@ from kubernetes.client import ( | |||||||
|     V1LabelSelector, |     V1LabelSelector, | ||||||
|     V1ObjectMeta, |     V1ObjectMeta, | ||||||
|     V1ObjectReference, |     V1ObjectReference, | ||||||
|  |     V1PodSecurityContext, | ||||||
|     V1PodSpec, |     V1PodSpec, | ||||||
|     V1PodTemplateSpec, |     V1PodTemplateSpec, | ||||||
|  |     V1SeccompProfile, | ||||||
|     V1SecretKeySelector, |     V1SecretKeySelector, | ||||||
|  |     V1SecurityContext, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| from authentik import __version__, get_full_version | from authentik import __version__, get_full_version | ||||||
| @ -103,6 +107,11 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|                         image_pull_secrets=[ |                         image_pull_secrets=[ | ||||||
|                             V1ObjectReference(name=secret) for secret in image_pull_secrets |                             V1ObjectReference(name=secret) for secret in image_pull_secrets | ||||||
|                         ], |                         ], | ||||||
|  |                         security_context=V1PodSecurityContext( | ||||||
|  |                             seccomp_profile=V1SeccompProfile( | ||||||
|  |                                 type="RuntimeDefault", | ||||||
|  |                             ), | ||||||
|  |                         ), | ||||||
|                         containers=[ |                         containers=[ | ||||||
|                             V1Container( |                             V1Container( | ||||||
|                                 name=str(self.outpost.type), |                                 name=str(self.outpost.type), | ||||||
| @ -146,6 +155,13 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|                                         ), |                                         ), | ||||||
|                                     ), |                                     ), | ||||||
|                                 ], |                                 ], | ||||||
|  |                                 security_context=V1SecurityContext( | ||||||
|  |                                     run_as_non_root=True, | ||||||
|  |                                     allow_privilege_escalation=False, | ||||||
|  |                                     capabilities=V1Capabilities( | ||||||
|  |                                         drop=["ALL"], | ||||||
|  |                                     ), | ||||||
|  |                                 ), | ||||||
|                             ) |                             ) | ||||||
|                         ], |                         ], | ||||||
|                     ), |                     ), | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								authentik/outposts/migrations/0020_alter_outpost_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								authentik/outposts/migrations/0020_alter_outpost_type.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | # Generated by Django 4.1.7 on 2023-03-20 10:58 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_outposts", "0019_alter_outpost_name_and_more"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="outpost", | ||||||
|  |             name="type", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[("proxy", "Proxy"), ("ldap", "Ldap"), ("radius", "Radius")], | ||||||
|  |                 default="proxy", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -94,6 +94,7 @@ class OutpostType(models.TextChoices): | |||||||
|  |  | ||||||
|     PROXY = "proxy" |     PROXY = "proxy" | ||||||
|     LDAP = "ldap" |     LDAP = "ldap" | ||||||
|  |     RADIUS = "radius" | ||||||
|  |  | ||||||
|  |  | ||||||
| def default_outpost_config(host: Optional[str] = None): | def default_outpost_config(host: Optional[str] = None): | ||||||
|  | |||||||
| @ -19,9 +19,9 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|         "schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"), |         "schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
|     "outpost_local_connection": { |     "outpost_connection_discovery": { | ||||||
|         "task": "authentik.outposts.tasks.outpost_local_connection", |         "task": "authentik.outposts.tasks.outpost_connection_discovery", | ||||||
|         "schedule": crontab(minute=fqdn_rand("outpost_local_connection"), hour="*/8"), |         "schedule": crontab(minute=fqdn_rand("outpost_connection_discovery"), hour="*/8"), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from urllib.parse import urlparse | |||||||
|  |  | ||||||
| import yaml | import yaml | ||||||
| from asgiref.sync import async_to_sync | from asgiref.sync import async_to_sync | ||||||
|  | from channels.layers import get_channel_layer | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db import DatabaseError, InternalError, ProgrammingError | from django.db import DatabaseError, InternalError, ProgrammingError | ||||||
| from django.db.models.base import Model | from django.db.models.base import Model | ||||||
| @ -42,7 +43,6 @@ from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesContro | |||||||
| from authentik.providers.proxy.controllers.docker import ProxyDockerController | from authentik.providers.proxy.controllers.docker import ProxyDockerController | ||||||
| from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController | from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.root.celery import CELERY_APP | ||||||
| from authentik.root.messages.storage import closing_send |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s" | CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s" | ||||||
| @ -214,50 +214,58 @@ def outpost_post_save(model_class: str, model_pk: Any): | |||||||
|             outpost_send_update(reverse) |             outpost_send_update(reverse) | ||||||
|  |  | ||||||
|  |  | ||||||
| def outpost_send_update(model_instace: Model): | def outpost_send_update(model_instance: Model): | ||||||
|     """Send outpost update to all registered outposts, regardless to which authentik |     """Send outpost update to all registered outposts, regardless to which authentik | ||||||
|     instance they are connected""" |     instance they are connected""" | ||||||
|     if isinstance(model_instace, OutpostModel): |     channel_layer = get_channel_layer() | ||||||
|         for outpost in model_instace.outpost_set.all(): |     if isinstance(model_instance, OutpostModel): | ||||||
|             _outpost_single_update(outpost) |         for outpost in model_instance.outpost_set.all(): | ||||||
|     elif isinstance(model_instace, Outpost): |             _outpost_single_update(outpost, channel_layer) | ||||||
|         _outpost_single_update(model_instace) |     elif isinstance(model_instance, Outpost): | ||||||
|  |         _outpost_single_update(model_instance, channel_layer) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _outpost_single_update(outpost: Outpost): | def _outpost_single_update(outpost: Outpost, layer=None): | ||||||
|     """Update outpost instances connected to a single outpost""" |     """Update outpost instances connected to a single outpost""" | ||||||
|     # Ensure token again, because this function is called when anything related to an |     # Ensure token again, because this function is called when anything related to an | ||||||
|     # OutpostModel is saved, so we can be sure permissions are right |     # OutpostModel is saved, so we can be sure permissions are right | ||||||
|     _ = outpost.token |     _ = outpost.token | ||||||
|     outpost.build_user_permissions(outpost.user) |     outpost.build_user_permissions(outpost.user) | ||||||
|  |     if not layer:  # pragma: no cover | ||||||
|  |         layer = get_channel_layer() | ||||||
|     for state in OutpostState.for_outpost(outpost): |     for state in OutpostState.for_outpost(outpost): | ||||||
|         for channel in state.channel_ids: |         for channel in state.channel_ids: | ||||||
|             LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost) |             LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost) | ||||||
|             async_to_sync(closing_send)(channel, {"type": "event.update"}) |             async_to_sync(layer.send)(channel, {"type": "event.update"}) | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task() | @CELERY_APP.task( | ||||||
| def outpost_local_connection(): |     base=MonitoredTask, | ||||||
|  |     bind=True, | ||||||
|  | ) | ||||||
|  | def outpost_connection_discovery(self: MonitoredTask): | ||||||
|     """Checks the local environment and create Service connections.""" |     """Checks the local environment and create Service connections.""" | ||||||
|  |     status = TaskResult(TaskResultStatus.SUCCESSFUL) | ||||||
|     if not CONFIG.y_bool("outposts.discover"): |     if not CONFIG.y_bool("outposts.discover"): | ||||||
|         LOGGER.info("Outpost integration discovery is disabled") |         status.messages.append("Outpost integration discovery is disabled") | ||||||
|  |         self.set_status(status) | ||||||
|         return |         return | ||||||
|     # Explicitly check against token filename, as that's |     # Explicitly check against token filename, as that's | ||||||
|     # only present when the integration is enabled |     # only present when the integration is enabled | ||||||
|     if Path(SERVICE_TOKEN_FILENAME).exists(): |     if Path(SERVICE_TOKEN_FILENAME).exists(): | ||||||
|         LOGGER.info("Detected in-cluster Kubernetes Config") |         status.messages.append("Detected in-cluster Kubernetes Config") | ||||||
|         if not KubernetesServiceConnection.objects.filter(local=True).exists(): |         if not KubernetesServiceConnection.objects.filter(local=True).exists(): | ||||||
|             LOGGER.debug("Created Service Connection for in-cluster") |             status.messages.append("Created Service Connection for in-cluster") | ||||||
|             KubernetesServiceConnection.objects.create( |             KubernetesServiceConnection.objects.create( | ||||||
|                 name="Local Kubernetes Cluster", local=True, kubeconfig={} |                 name="Local Kubernetes Cluster", local=True, kubeconfig={} | ||||||
|             ) |             ) | ||||||
|     # For development, check for the existence of a kubeconfig file |     # For development, check for the existence of a kubeconfig file | ||||||
|     kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser() |     kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser() | ||||||
|     if kubeconfig_path.exists(): |     if kubeconfig_path.exists(): | ||||||
|         LOGGER.info("Detected kubeconfig") |         status.messages.append("Detected kubeconfig") | ||||||
|         kubeconfig_local_name = f"k8s-{gethostname()}" |         kubeconfig_local_name = f"k8s-{gethostname()}" | ||||||
|         if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists(): |         if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists(): | ||||||
|             LOGGER.debug("Creating kubeconfig Service Connection") |             status.messages.append("Creating kubeconfig Service Connection") | ||||||
|             with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig: |             with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig: | ||||||
|                 KubernetesServiceConnection.objects.create( |                 KubernetesServiceConnection.objects.create( | ||||||
|                     name=kubeconfig_local_name, |                     name=kubeconfig_local_name, | ||||||
| @ -266,11 +274,12 @@ def outpost_local_connection(): | |||||||
|     unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path |     unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path | ||||||
|     socket = Path(unix_socket_path) |     socket = Path(unix_socket_path) | ||||||
|     if socket.exists() and access(socket, R_OK): |     if socket.exists() and access(socket, R_OK): | ||||||
|         LOGGER.info("Detected local docker socket") |         status.messages.append("Detected local docker socket") | ||||||
|         if len(DockerServiceConnection.objects.filter(local=True)) == 0: |         if len(DockerServiceConnection.objects.filter(local=True)) == 0: | ||||||
|             LOGGER.debug("Created Service Connection for docker") |             status.messages.append("Created Service Connection for docker") | ||||||
|             DockerServiceConnection.objects.create( |             DockerServiceConnection.objects.create( | ||||||
|                 name="Local Docker connection", |                 name="Local Docker connection", | ||||||
|                 local=True, |                 local=True, | ||||||
|                 url=unix_socket_path, |                 url=unix_socket_path, | ||||||
|             ) |             ) | ||||||
|  |     self.set_status(status) | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								authentik/outposts/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								authentik/outposts/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | """Outpost Websocket URLS""" | ||||||
|  | from django.urls import path | ||||||
|  |  | ||||||
|  | from authentik.outposts.channels import OutpostConsumer | ||||||
|  |  | ||||||
|  | websocket_urlpatterns = [ | ||||||
|  |     path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi()), | ||||||
|  | ] | ||||||
| @ -9,8 +9,6 @@ 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() | ||||||
| @ -32,22 +30,11 @@ class PolicyEvaluator(BaseEvaluator): | |||||||
|         # update website/docs/expressions/_functions.md |         # update website/docs/expressions/_functions.md | ||||||
|         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 |  | ||||||
|  |  | ||||||
|     def expr_func_message(self, message: str): |     def expr_func_message(self, message: str): | ||||||
|         """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 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)""" | ||||||
|         # update website/docs/expressions/_objects.md |         # update website/docs/expressions/_objects.md | ||||||
| @ -83,6 +70,7 @@ class PolicyEvaluator(BaseEvaluator): | |||||||
|             return PolicyResult(False, str(exc)) |             return PolicyResult(False, str(exc)) | ||||||
|         else: |         else: | ||||||
|             policy_result = PolicyResult(False, *self._messages) |             policy_result = PolicyResult(False, *self._messages) | ||||||
|  |             policy_result.raw_result = result | ||||||
|             if result is None: |             if result is None: | ||||||
|                 LOGGER.warning( |                 LOGGER.warning( | ||||||
|                     "Expression policy returned None", |                     "Expression policy returned None", | ||||||
|  | |||||||
| @ -54,6 +54,7 @@ class TestPasswordPolicyFlow(FlowTestCase): | |||||||
|             component="ak-stage-prompt", |             component="ak-stage-prompt", | ||||||
|             fields=[ |             fields=[ | ||||||
|                 { |                 { | ||||||
|  |                     "choices": None, | ||||||
|                     "field_key": "password", |                     "field_key": "password", | ||||||
|                     "label": "PASSWORD_LABEL", |                     "label": "PASSWORD_LABEL", | ||||||
|                     "order": 0, |                     "order": 0, | ||||||
|  | |||||||
| @ -132,9 +132,9 @@ class TestPolicyProcess(TestCase): | |||||||
|         ) |         ) | ||||||
|         binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test")) |         binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test")) | ||||||
|  |  | ||||||
|         http_request = self.factory.get(reverse("authentik_core:impersonate-end")) |         http_request = self.factory.get(reverse("authentik_api:user-impersonate-end")) | ||||||
|         http_request.user = self.user |         http_request.user = self.user | ||||||
|         http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end")) |         http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end")) | ||||||
|  |  | ||||||
|         request = PolicyRequest(self.user) |         request = PolicyRequest(self.user) | ||||||
|         request.set_http_request(http_request) |         request.set_http_request(http_request) | ||||||
|  | |||||||
| @ -69,10 +69,11 @@ class PolicyRequest: | |||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| class PolicyResult: | class PolicyResult: | ||||||
|     """Small data-class to hold policy results""" |     """Result from evaluating a policy.""" | ||||||
|  |  | ||||||
|     passing: bool |     passing: bool | ||||||
|     messages: tuple[str, ...] |     messages: tuple[str, ...] | ||||||
|  |     raw_result: Any | ||||||
|  |  | ||||||
|     source_binding: Optional["PolicyBinding"] |     source_binding: Optional["PolicyBinding"] | ||||||
|     source_results: Optional[list["PolicyResult"]] |     source_results: Optional[list["PolicyResult"]] | ||||||
| @ -83,6 +84,7 @@ class PolicyResult: | |||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.passing = passing |         self.passing = passing | ||||||
|         self.messages = messages |         self.messages = messages | ||||||
|  |         self.raw_result = None | ||||||
|         self.source_binding = None |         self.source_binding = None | ||||||
|         self.source_results = [] |         self.source_results = [] | ||||||
|         self.log_messages = [] |         self.log_messages = [] | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ class LDAPProviderSerializer(ProviderSerializer): | |||||||
|             "search_mode", |             "search_mode", | ||||||
|             "bind_mode", |             "bind_mode", | ||||||
|         ] |         ] | ||||||
|  |         extra_kwargs = ProviderSerializer.Meta.extra_kwargs | ||||||
|  |  | ||||||
|  |  | ||||||
| class LDAPProviderViewSet(UsedByMixin, ModelViewSet): | class LDAPProviderViewSet(UsedByMixin, ModelViewSet): | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ class OAuth2ProviderSerializer(ProviderSerializer): | |||||||
|             "issuer_mode", |             "issuer_mode", | ||||||
|             "jwks_sources", |             "jwks_sources", | ||||||
|         ] |         ] | ||||||
|  |         extra_kwargs = ProviderSerializer.Meta.extra_kwargs | ||||||
|  |  | ||||||
|  |  | ||||||
| class OAuth2ProviderSetupURLs(PassiveSerializer): | class OAuth2ProviderSetupURLs(PassiveSerializer): | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ | |||||||
| import django.utils.timezone | import django.utils.timezone | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
|  |  | ||||||
|  | import authentik.providers.oauth2.models | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|     dependencies = [ |     dependencies = [ | ||||||
| @ -37,4 +39,14 @@ class Migration(migrations.Migration): | |||||||
|             ), |             ), | ||||||
|             preserve_default=False, |             preserve_default=False, | ||||||
|         ), |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="oauth2provider", | ||||||
|  |             name="client_secret", | ||||||
|  |             field=models.CharField( | ||||||
|  |                 blank=True, | ||||||
|  |                 default=authentik.providers.oauth2.models.generate_client_secret, | ||||||
|  |                 max_length=255, | ||||||
|  |                 verbose_name="Client Secret", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -27,6 +27,11 @@ from authentik.providers.oauth2.id_token import IDToken, SubModes | |||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def generate_client_secret() -> str: | ||||||
|  |     """Generate client secret with adequate length""" | ||||||
|  |     return generate_id(128) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ClientTypes(models.TextChoices): | class ClientTypes(models.TextChoices): | ||||||
|     """Confidential clients are capable of maintaining the confidentiality |     """Confidential clients are capable of maintaining the confidentiality | ||||||
|     of their credentials. Public clients are incapable.""" |     of their credentials. Public clients are incapable.""" | ||||||
| @ -132,7 +137,7 @@ class OAuth2Provider(Provider): | |||||||
|         max_length=255, |         max_length=255, | ||||||
|         blank=True, |         blank=True, | ||||||
|         verbose_name=_("Client Secret"), |         verbose_name=_("Client Secret"), | ||||||
|         default=generate_key, |         default=generate_client_secret, | ||||||
|     ) |     ) | ||||||
|     redirect_uris = models.TextField( |     redirect_uris = models.TextField( | ||||||
|         default="", |         default="", | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from rest_framework.test import APITestCase | |||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key |  | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -18,8 +17,6 @@ class TestAPI(APITestCase): | |||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://testserver", |             redirect_uris="http://testserver", | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from authentik.core.models import Application | |||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id | ||||||
| from authentik.lib.utils.time import timedelta_from_string | from authentik.lib.utils.time import timedelta_from_string | ||||||
| from authentik.providers.oauth2.constants import TOKEN_TYPE | from authentik.providers.oauth2.constants import TOKEN_TYPE | ||||||
| from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError | from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError | ||||||
| @ -298,7 +298,6 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         provider: OAuth2Provider = OAuth2Provider.objects.create( |         provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris="http://localhost", |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -355,13 +354,67 @@ class TestAuthorize(OAuthTestCase): | |||||||
|                 delta=5, |                 delta=5, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     def test_full_fragment_code(self): | ||||||
|  |         """Test full authorization""" | ||||||
|  |         flow = create_test_flow() | ||||||
|  |         provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=flow, | ||||||
|  |             redirect_uris="http://localhost", | ||||||
|  |             signing_key=self.keypair, | ||||||
|  |         ) | ||||||
|  |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  |         state = generate_id() | ||||||
|  |         user = create_test_admin_user() | ||||||
|  |         self.client.force_login(user) | ||||||
|  |         with patch( | ||||||
|  |             "authentik.providers.oauth2.id_token.get_login_event", | ||||||
|  |             MagicMock( | ||||||
|  |                 return_value=Event( | ||||||
|  |                     action=EventAction.LOGIN, | ||||||
|  |                     context={PLAN_CONTEXT_METHOD: "password"}, | ||||||
|  |                     created=now(), | ||||||
|  |                 ) | ||||||
|  |             ), | ||||||
|  |         ): | ||||||
|  |             # Step 1, initiate params and get redirect to flow | ||||||
|  |             self.client.get( | ||||||
|  |                 reverse("authentik_providers_oauth2:authorize"), | ||||||
|  |                 data={ | ||||||
|  |                     "response_type": "code", | ||||||
|  |                     "response_mode": "fragment", | ||||||
|  |                     "client_id": "test", | ||||||
|  |                     "state": state, | ||||||
|  |                     "scope": "openid", | ||||||
|  |                     "redirect_uri": "http://localhost", | ||||||
|  |                     "nonce": generate_id(), | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             response = self.client.get( | ||||||
|  |                 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |             ) | ||||||
|  |             code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() | ||||||
|  |             self.assertJSONEqual( | ||||||
|  |                 response.content.decode(), | ||||||
|  |                 { | ||||||
|  |                     "component": "xak-flow-redirect", | ||||||
|  |                     "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                     "to": (f"http://localhost#code={code.code}" f"&state={state}"), | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             self.assertAlmostEqual( | ||||||
|  |                 code.expires.timestamp() - now().timestamp(), | ||||||
|  |                 timedelta_from_string(provider.access_code_validity).total_seconds(), | ||||||
|  |                 delta=5, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     def test_full_form_post_id_token(self): |     def test_full_form_post_id_token(self): | ||||||
|         """Test full authorization (form_post response)""" |         """Test full authorization (form_post response)""" | ||||||
|         flow = create_test_flow() |         flow = create_test_flow() | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |             client_id=generate_id(), | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris="http://localhost", |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -411,7 +464,6 @@ class TestAuthorize(OAuthTestCase): | |||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |             client_id=generate_id(), | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=flow, |             authorization_flow=flow, | ||||||
|             redirect_uris="http://localhost", |             redirect_uris="http://localhost", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from django.utils import timezone | |||||||
|  |  | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT | from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT | ||||||
| from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken | from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
| @ -21,8 +21,6 @@ class TesOAuth2Introspection(OAuthTestCase): | |||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="", |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from django.utils import timezone | |||||||
|  |  | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken | from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
| @ -20,8 +20,6 @@ class TesOAuth2Revoke(OAuthTestCase): | |||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="", |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|  | |||||||
| @ -38,8 +38,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://TestServer", |             redirect_uris="http://TestServer", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -67,8 +65,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://testserver", |             redirect_uris="http://testserver", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -90,8 +86,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -120,8 +114,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -163,8 +155,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -215,8 +205,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
| @ -263,8 +251,6 @@ class TestToken(OAuthTestCase): | |||||||
|         """test request param""" |         """test request param""" | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://testserver", |             redirect_uris="http://testserver", | ||||||
|             signing_key=self.keypair, |             signing_key=self.keypair, | ||||||
|  | |||||||
| @ -8,7 +8,6 @@ from jwt import decode | |||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents | from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key |  | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.providers.oauth2.constants import ( | from authentik.providers.oauth2.constants import ( | ||||||
|     GRANT_TYPE_CLIENT_CREDENTIALS, |     GRANT_TYPE_CLIENT_CREDENTIALS, | ||||||
| @ -31,8 +30,6 @@ class TestTokenClientCredentials(OAuthTestCase): | |||||||
|         self.factory = RequestFactory() |         self.factory = RequestFactory() | ||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://testserver", |             redirect_uris="http://testserver", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from jwt import decode | |||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application, Group | from authentik.core.models import Application, Group | ||||||
| from authentik.core.tests.utils import create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id | ||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.providers.oauth2.constants import ( | from authentik.providers.oauth2.constants import ( | ||||||
|     GRANT_TYPE_CLIENT_CREDENTIALS, |     GRANT_TYPE_CLIENT_CREDENTIALS, | ||||||
| @ -39,7 +39,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase): | |||||||
|             slug=generate_id(), |             slug=generate_id(), | ||||||
|             provider_type="openidconnect", |             provider_type="openidconnect", | ||||||
|             consumer_key=generate_id(), |             consumer_key=generate_id(), | ||||||
|             consumer_secret=generate_key(), |             consumer_secret=generate_id(), | ||||||
|             authorization_url="http://foo", |             authorization_url="http://foo", | ||||||
|             access_token_url=f"http://{generate_id()}", |             access_token_url=f"http://{generate_id()}", | ||||||
|             profile_url="http://foo", |             profile_url="http://foo", | ||||||
| @ -52,8 +52,6 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase): | |||||||
|  |  | ||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://testserver", |             redirect_uris="http://testserver", | ||||||
|             signing_key=self.cert, |             signing_key=self.cert, | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ from django.urls import reverse | |||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key | from authentik.lib.generators import generate_code_fixed_length, generate_id | ||||||
| from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE | from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE | ||||||
| from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
| @ -22,8 +22,6 @@ class TestTokenDeviceCode(OAuthTestCase): | |||||||
|         self.factory = RequestFactory() |         self.factory = RequestFactory() | ||||||
|         self.provider = OAuth2Provider.objects.create( |         self.provider = OAuth2Provider.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://testserver", |             redirect_uris="http://testserver", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from authentik.blueprints.tests import apply_blueprint | |||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id | ||||||
| from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping | from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping | ||||||
| from authentik.providers.oauth2.tests.utils import OAuthTestCase | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  |  | ||||||
| @ -23,8 +23,6 @@ class TestUserinfo(OAuthTestCase): | |||||||
|         self.app = Application.objects.create(name=generate_id(), slug=generate_id()) |         self.app = Application.objects.create(name=generate_id(), slug=generate_id()) | ||||||
|         self.provider: OAuth2Provider = OAuth2Provider.objects.create( |         self.provider: OAuth2Provider = OAuth2Provider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             client_id=generate_id(), |  | ||||||
|             client_secret=generate_key(), |  | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="", |             redirect_uris="", | ||||||
|             signing_key=create_test_cert(), |             signing_key=create_test_cert(), | ||||||
|  | |||||||
| @ -514,7 +514,12 @@ class OAuthFulfillmentStage(StageView): | |||||||
|                 return urlunsplit(uri) |                 return urlunsplit(uri) | ||||||
|  |  | ||||||
|             if self.params.response_mode == ResponseMode.FRAGMENT: |             if self.params.response_mode == ResponseMode.FRAGMENT: | ||||||
|                 query_fragment = self.create_implicit_response(code) |                 query_fragment = {} | ||||||
|  |                 if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]: | ||||||
|  |                     query_fragment["code"] = code.code | ||||||
|  |                     query_fragment["state"] = [str(self.params.state) if self.params.state else ""] | ||||||
|  |                 else: | ||||||
|  |                     query_fragment = self.create_implicit_response(code) | ||||||
|  |  | ||||||
|                 uri = uri._replace( |                 uri = uri._replace( | ||||||
|                     fragment=uri.fragment + urlencode(query_fragment, doseq=True), |                     fragment=uri.fragment + urlencode(query_fragment, doseq=True), | ||||||
|  | |||||||
| @ -95,6 +95,7 @@ class ProxyProviderSerializer(ProviderSerializer): | |||||||
|             "refresh_token_validity", |             "refresh_token_validity", | ||||||
|             "outpost_set", |             "outpost_set", | ||||||
|         ] |         ] | ||||||
|  |         extra_kwargs = ProviderSerializer.Meta.extra_kwargs | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProxyProviderViewSet(UsedByMixin, ModelViewSet): | class ProxyProviderViewSet(UsedByMixin, ModelViewSet): | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								authentik/providers/radius/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/providers/radius/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										65
									
								
								authentik/providers/radius/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								authentik/providers/radius/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | """RadiusProvider API Views""" | ||||||
|  | from rest_framework.fields import CharField | ||||||
|  | from rest_framework.serializers import ModelSerializer | ||||||
|  | from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet | ||||||
|  |  | ||||||
|  | from authentik.core.api.providers import ProviderSerializer | ||||||
|  | from authentik.core.api.used_by import UsedByMixin | ||||||
|  | from authentik.providers.radius.models import RadiusProvider | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RadiusProviderSerializer(ProviderSerializer): | ||||||
|  |     """RadiusProvider Serializer""" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = RadiusProvider | ||||||
|  |         fields = ProviderSerializer.Meta.fields + [ | ||||||
|  |             "client_networks", | ||||||
|  |             # Shared secret is not a write-only field, as | ||||||
|  |             # an admin might have to view it | ||||||
|  |             "shared_secret", | ||||||
|  |         ] | ||||||
|  |         extra_kwargs = ProviderSerializer.Meta.extra_kwargs | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RadiusProviderViewSet(UsedByMixin, ModelViewSet): | ||||||
|  |     """RadiusProvider Viewset""" | ||||||
|  |  | ||||||
|  |     queryset = RadiusProvider.objects.all() | ||||||
|  |     serializer_class = RadiusProviderSerializer | ||||||
|  |     ordering = ["name"] | ||||||
|  |     search_fields = ["name", "client_networks"] | ||||||
|  |     filterset_fields = { | ||||||
|  |         "application": ["isnull"], | ||||||
|  |         "name": ["iexact"], | ||||||
|  |         "authorization_flow__slug": ["iexact"], | ||||||
|  |         "client_networks": ["iexact"], | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RadiusOutpostConfigSerializer(ModelSerializer): | ||||||
|  |     """RadiusProvider Serializer""" | ||||||
|  |  | ||||||
|  |     application_slug = CharField(source="application.slug") | ||||||
|  |     auth_flow_slug = CharField(source="authorization_flow.slug") | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = RadiusProvider | ||||||
|  |         fields = [ | ||||||
|  |             "pk", | ||||||
|  |             "name", | ||||||
|  |             "application_slug", | ||||||
|  |             "auth_flow_slug", | ||||||
|  |             "client_networks", | ||||||
|  |             "shared_secret", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RadiusOutpostConfigViewSet(ReadOnlyModelViewSet): | ||||||
|  |     """RadiusProvider Viewset""" | ||||||
|  |  | ||||||
|  |     queryset = RadiusProvider.objects.filter(application__isnull=False) | ||||||
|  |     serializer_class = RadiusOutpostConfigSerializer | ||||||
|  |     ordering = ["name"] | ||||||
|  |     search_fields = ["name"] | ||||||
|  |     filterset_fields = ["name"] | ||||||
							
								
								
									
										10
									
								
								authentik/providers/radius/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								authentik/providers/radius/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | """authentik radius provider app config""" | ||||||
|  | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthentikProviderRadiusConfig(AppConfig): | ||||||
|  |     """authentik radius provider app config""" | ||||||
|  |  | ||||||
|  |     name = "authentik.providers.radius" | ||||||
|  |     label = "authentik_providers_radius" | ||||||
|  |     verbose_name = "authentik Providers.Radius" | ||||||
							
								
								
									
										0
									
								
								authentik/providers/radius/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/providers/radius/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	