Compare commits
	
		
			432 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 70c25692eb | |||
| 71b31a2812 | |||
| 3208358a03 | |||
| a6a8eddf7c | |||
| 8c0a87b710 | |||
| 2f88c435fb | |||
| 5cad59a9f8 | |||
| 5ac6a6910e | |||
| d751a7fc4c | |||
| f1fd223bc7 | |||
| e75712fa09 | |||
| 1b87375661 | |||
| 545a114450 | |||
| 02b06838e2 | |||
| 6868b7722c | |||
| 1e303b515b | |||
| 34a9a6a389 | |||
| 7a1935b4e2 | |||
| bf60b33d03 | |||
| 9bb50fd556 | |||
| 5e7521915a | |||
| 7b0cda3a6a | |||
| db5279f952 | |||
| 9fc072e4df | |||
| 55ea9afeec | |||
| 9485f0b8cc | |||
| fabdb6448f | |||
| e629079352 | |||
| e6dfa8294e | |||
| e5a5a5c603 | |||
| 4d07da5ffa | |||
| 5b4f34fd5f | |||
| 2e05047151 | |||
| 459a6ea437 | |||
| ea7f9f291f | |||
| 241d790e69 | |||
| 83e08f12ae | |||
| 6526659b51 | |||
| 6c3b7c8d3e | |||
| d51ecc4554 | |||
| ef63e35ad2 | |||
| 4e9176ed2e | |||
| d1296e9cc7 | |||
| d85e0593f1 | |||
| 20c1f15dc0 | |||
| c864f4e312 | |||
| 202ad1a3ac | |||
| 979a5f800e | |||
| c151faeff6 | |||
| b3a3852a54 | |||
| e401b4e74e | |||
| 9538ad5710 | |||
| 49bf82a0a4 | |||
| e6fdec4c8e | |||
| 73b87a5e3d | |||
| 303b847cdc | |||
| 0386c0dd7b | |||
| 7f1b9cdeb2 | |||
| 252bb04dd3 | |||
| 3fbcfb48fb | |||
| 69f7198976 | |||
| c74c8b2083 | |||
| 63d4f598e4 | |||
| ded6b6f937 | |||
| 225099b1a1 | |||
| 6b7a32548d | |||
| c71d415456 | |||
| c03f0d1d7c | |||
| ac9cac302c | |||
| 701c140cfd | |||
| ca5761652c | |||
| 553872e8dd | |||
| adc9b67a9c | |||
| fa2ff5fc2b | |||
| d5cab5d580 | |||
| 9e3b5d313b | |||
| be8b2bf6f6 | |||
| 3f8cd7ff13 | |||
| b266a2cdfb | |||
| 9a15a66d85 | |||
| 446f104c90 | |||
| 2cad9a3d07 | |||
| ee48b8c225 | |||
| a91649a7d4 | |||
| ca89201bd8 | |||
| e3a8fc0746 | |||
| 5e3a6b802b | |||
| e8d9f992b9 | |||
| 260b2c8ca8 | |||
| 751e77fa9e | |||
| 86c2a5d69d | |||
| 1a02049104 | |||
| 32934fcd38 | |||
| d84d7c26ca | |||
| 2f6e6a3123 | |||
| 36b674349a | |||
| 038ef67745 | |||
| 53831fa354 | |||
| be39673f29 | |||
| 0f8dbfcc9c | |||
| ba57bf4fa2 | |||
| b1c9126832 | |||
| e674f03064 | |||
| 08451c15f4 | |||
| 99d161e212 | |||
| 940ccf9ea8 | |||
| 08cce2ca4e | |||
| 4acbda2b77 | |||
| 83cfb5f8c2 | |||
| 0d370ef0a9 | |||
| a335ca0895 | |||
| 8a666535a8 | |||
| e6431593f7 | |||
| 928c2bf0d6 | |||
| 68388e9551 | |||
| 5d26fa0403 | |||
| 42f9ba8efe | |||
| 0440ad7c09 | |||
| 3ebc531ae2 | |||
| ca3b5fa2a2 | |||
| 0f0a5b0621 | |||
| 51835887ab | |||
| 09bcbcc2ac | |||
| 8a76d6a21b | |||
| 48ab436444 | |||
| 18a53a9e23 | |||
| 6725569ba8 | |||
| 812be495a5 | |||
| dbc3df1f63 | |||
| 07b001bc2b | |||
| c012bed379 | |||
| d330e9ee7f | |||
| be21a5d172 | |||
| ea2f623955 | |||
| 6fc38436f4 | |||
| 35faf269db | |||
| e56c3fc54c | |||
| 5891fb3ad6 | |||
| 1041718e27 | |||
| 2507c0eec9 | |||
| 5ea9601062 | |||
| c0e6a6c614 | |||
| 4523550422 | |||
| 988cf15b71 | |||
| 6ae660aea4 | |||
| f201ce8059 | |||
| 59624ed45c | |||
| 3e78baf2d7 | |||
| 08c67b2a2c | |||
| 01d29134b9 | |||
| 55250e88e5 | |||
| f1b100c8a5 | |||
| 19708bc67b | |||
| 40a885aaaa | |||
| c529340d6c | |||
| c317efa14c | |||
| 379fcf9c1f | |||
| e10a7b48b7 | |||
| 3e666de91d | |||
| 333758d91f | |||
| 50678a9e2e | |||
| eb8f52b870 | |||
| 3ee90712b2 | |||
| e4eadf8080 | |||
| 26ebaf16fc | |||
| d0ed372af0 | |||
| cc8b2d7dfe | |||
| 61a212371f | |||
| 9ce49c2089 | |||
| 34c45900c2 | |||
| bf7d110af3 | |||
| 4e5eeacf0a | |||
| e7b498e8b4 | |||
| b55cb2b40c | |||
| 25c001f2cd | |||
| 2a409215d3 | |||
| ad8ee83697 | |||
| d1d28722d2 | |||
| 1efd09fcd5 | |||
| 35f0e6b88d | |||
| a6e528d209 | |||
| bb2c4423b0 | |||
| ad9f29566b | |||
| e76bb6bc13 | |||
| a68642779d | |||
| 3c04fcaa9f | |||
| 5955d28073 | |||
| a6fb6161d7 | |||
| 6b0e0610c6 | |||
| 2c70301f56 | |||
| 07b9923bf6 | |||
| 8b3923200d | |||
| 3dcd67c1a3 | |||
| 2a9feafb90 | |||
| 580e88c6fc | |||
| d82c01aa61 | |||
| 1af3357826 | |||
| ed49d7824e | |||
| 378402fcf0 | |||
| 50f0c11c0b | |||
| 58712828a4 | |||
| b2b9093c95 | |||
| afa2afe1d4 | |||
| d7631e8af0 | |||
| 6e625f7400 | |||
| f54ead2b45 | |||
| c4e4e17f93 | |||
| 43c87f87c3 | |||
| 4da0c81f44 | |||
| 9b70aaa717 | |||
| 5769eb277c | |||
| 26f60b3e85 | |||
| 7d8ed06539 | |||
| 4d858c64e0 | |||
| 6f0792ccfe | |||
| 04f06e00ff | |||
| 776c3128b8 | |||
| e9e0992dce | |||
| 69af788b0f | |||
| ceace0282b | |||
| ccef7b4233 | |||
| cad6c42fdd | |||
| d2abe6d455 | |||
| 68d120b3b4 | |||
| 48c0c0baca | |||
| 7b29a1e485 | |||
| fe28d216fe | |||
| e36fb6641e | |||
| 972471ce79 | |||
| 38edd76949 | |||
| cd07c12c1b | |||
| 3ce8b836dc | |||
| d27dfcc1e3 | |||
| 1d5958a78f | |||
| b6e0a1d8f4 | |||
| 2a122845d9 | |||
| 21c7787eed | |||
| fae4d34131 | |||
| 7ff7bfeb58 | |||
| 983604265b | |||
| f8d6daa928 | |||
| 6fc26aca72 | |||
| 29da7dd8d6 | |||
| 91ca90f700 | |||
| b3c8ffb96c | |||
| b35d9ae8b0 | |||
| 302b047f1a | |||
| dcd80c6d63 | |||
| d741ed430a | |||
| 8436738b0f | |||
| 5b150657f5 | |||
| f89479caf3 | |||
| 2f3bf5efe7 | |||
| 5fb07acf54 | |||
| 99d0d4e8de | |||
| afc5dc5543 | |||
| 9341787fe7 | |||
| 6c9b3ebd2b | |||
| a525d6c3a9 | |||
| b59b9314e4 | |||
| 7687b744cc | |||
| 9fb41b8d10 | |||
| 51ffdcb5cb | |||
| 4d6cd4c57d | |||
| 41c5f01422 | |||
| e567cd5580 | |||
| 5f81909bab | |||
| d03b43605e | |||
| ea187d4e81 | |||
| 502ac51fa7 | |||
| 4bc6fd28d4 | |||
| 820c9e7d06 | |||
| e5a8714e6a | |||
| d56d6ea3a9 | |||
| 5f58a4566c | |||
| d616bdd5d6 | |||
| 5112ef9331 | |||
| 7a49377caf | |||
| 5b3941a425 | |||
| c1ab5c5556 | |||
| 3282b34431 | |||
| 392d9bb10b | |||
| 82f6c515ea | |||
| d67d5f73c5 | |||
| 799d186510 | |||
| 3983b7fbe4 | |||
| d75284a587 | |||
| 71e4936dc3 | |||
| 9d3b6f7a4d | |||
| 003df44a34 | |||
| a7598c6ee5 | |||
| 0891e43040 | |||
| 1f49aea48d | |||
| 499b52df6a | |||
| b8a566f4a0 | |||
| aa0e8edb8b | |||
| 0e35bb18c7 | |||
| 4a06ebf4f9 | |||
| 11584af425 | |||
| a31da9e1d3 | |||
| 8d6d49834b | |||
| 2825710262 | |||
| 7346ccf2b7 | |||
| 57072dd6ce | |||
| fec098a823 | |||
| 73950b72e5 | |||
| b40afb9b7d | |||
| 1f783dfc01 | |||
| 7ccf8bcdc8 | |||
| 76131e40ec | |||
| 5955394c1d | |||
| a8998a6356 | |||
| dc75d7b7f0 | |||
| 34a191f216 | |||
| 299931985e | |||
| b946fbf9e7 | |||
| e20bb7d636 | |||
| 5db3409efc | |||
| 649db054a6 | |||
| 4f5e1fb86b | |||
| 15d5b91642 | |||
| e9abc25b92 | |||
| dc930c0cdf | |||
| 464a1c0536 | |||
| 837d2f6fab | |||
| 8f00d73512 | |||
| b75feab709 | |||
| 9c8433ec4d | |||
| ef080900a4 | |||
| 10b45a8dea | |||
| c43ac1f704 | |||
| 14d702450a | |||
| 0a1a2a035e | |||
| ace777ebbe | |||
| 8a6879afa5 | |||
| fdc7f14056 | |||
| 8be80aaf9d | |||
| e476f2dda2 | |||
| 5d48cfab14 | |||
| 1f22f0e7bb | |||
| ce082ead5e | |||
| dd2cd09637 | |||
| 828fe07fca | |||
| a074ea70e9 | |||
| 84ce2c1df2 | |||
| 8628595590 | |||
| 7b8e5c4272 | |||
| caa5dc1d14 | |||
| f328b21e89 | |||
| 52abd959eb | |||
| a0cd17a257 | |||
| 32c5bf04b8 | |||
| 766c4873a0 | |||
| 240136154b | |||
| 78dd7b0341 | |||
| 0021a93952 | |||
| 67240fb9ad | |||
| 4add0bbe86 | |||
| d2dd7d1366 | |||
| 476e57daa2 | |||
| 4eb8a0dcd1 | |||
| 60615c9f3e | |||
| b5b8573d87 | |||
| 2e44c1cdfc | |||
| 31909a4d78 | |||
| 4a444e667a | |||
| f67b57e369 | |||
| 6be19962d2 | |||
| 262a9fa2a0 | |||
| e8ba159756 | |||
| 0b03d66a2f | |||
| 7c858c9626 | |||
| 71b6839d03 | |||
| ada49c077a | |||
| 7880c7fb98 | |||
| 2b48ba4103 | |||
| 5e67f68f2b | |||
| 1992b89154 | |||
| 9ab2088ab7 | |||
| a9d0d96418 | |||
| c476503594 | |||
| de74f3ec1f | |||
| ce98255607 | |||
| 53b9e5b93f | |||
| 7aeb390eac | |||
| 5df9ad63cf | |||
| e4400476a2 | |||
| ef3c01ec34 | |||
| b136d3bc69 | |||
| c34fcc73dc | |||
| 11b09c4ebd | |||
| e32070ddeb | |||
| 33a8cea007 | |||
| d01fd7cdb7 | |||
| 1770e42cbf | |||
| 2fed739be7 | |||
| aa820b2b4d | |||
| 582d2eb5eb | |||
| c5e2635903 | |||
| cfe0a7a694 | |||
| c579540473 | |||
| 35f2b06611 | |||
| 9c4f025d71 | |||
| d8b8e8a5a3 | |||
| ec34c3eb75 | |||
| 0554c94c53 | |||
| 19a663a645 | |||
| e72881b2a9 | |||
| 4452ff171e | |||
| 39bdc3a9a9 | |||
| 33bb6edf8c | |||
| 2eb18ff5e6 | |||
| aeb1b5e8f2 | |||
| bd8447d5a7 | |||
| 35fad191b8 | |||
| 40a6f15cf1 | |||
| 420465981b | |||
| 4f9f936a7f | |||
| 85c9fbe763 | |||
| 3d9874be69 | |||
| 9742d19729 | |||
| 5a25e6d697 | |||
| 7798a046db | |||
| 7a562fe8c0 | |||
| 6821679fbc | |||
| 513d3c1c31 | |||
| 30cb468ec5 | |||
| 8b66fa55a6 | |||
| 55bb9b6643 | |||
| 1b79fad6cf | |||
| f9976492e7 | |||
| 2fd0e46378 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2021.4.1-rc2 | current_version = 2021.5.1-rc1 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | ||||||
| @ -19,20 +19,14 @@ values = | |||||||
|  |  | ||||||
| [bumpversion:file:website/docs/installation/docker-compose.md] | [bumpversion:file:website/docs/installation/docker-compose.md] | ||||||
|  |  | ||||||
| [bumpversion:file:website/docs/installation/kubernetes.md] |  | ||||||
|  |  | ||||||
| [bumpversion:file:docker-compose.yml] | [bumpversion:file:docker-compose.yml] | ||||||
|  |  | ||||||
| [bumpversion:file:helm/values.yaml] |  | ||||||
|  |  | ||||||
| [bumpversion:file:helm/README.md] |  | ||||||
|  |  | ||||||
| [bumpversion:file:helm/Chart.yaml] |  | ||||||
|  |  | ||||||
| [bumpversion:file:.github/workflows/release.yml] | [bumpversion:file:.github/workflows/release.yml] | ||||||
|  |  | ||||||
| [bumpversion:file:authentik/__init__.py] | [bumpversion:file:authentik/__init__.py] | ||||||
|  |  | ||||||
|  | [bumpversion:file:internal/constants/constants.go] | ||||||
|  |  | ||||||
| [bumpversion:file:outpost/pkg/version.go] | [bumpversion:file:outpost/pkg/version.go] | ||||||
|  |  | ||||||
| [bumpversion:file:web/src/constants.ts] | [bumpversion:file:web/src/constants.ts] | ||||||
|  | |||||||
							
								
								
									
										116
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										116
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @ -10,21 +10,25 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v1 |       - uses: actions/checkout@v1 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v1 | ||||||
|       - name: Docker Login Registry |       - name: Docker Login Registry | ||||||
|         env: |         uses: docker/login-action@v1 | ||||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} |         with: | ||||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} |           username: ${{ secrets.DOCKER_PASSWORD }} | ||||||
|         run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD |           password: ${{ secrets.DOCKER_USERNAME }} | ||||||
|       - name: Building Docker Image |       - name: Building Docker Image | ||||||
|         run: docker build |         uses: docker/build-push-action@v2 | ||||||
|           --no-cache |         with: | ||||||
|           -t beryju/authentik:2021.4.1-rc2 |           push: true | ||||||
|           -t beryju/authentik:latest |           tags: | | ||||||
|           -f Dockerfile . |             beryju/authentik:2021.5.1-rc1, | ||||||
|       - name: Push Docker Container to Registry (versioned) |             beryju/authentik:latest, | ||||||
|         run: docker push beryju/authentik:2021.4.1-rc2 |             ghcr.io/goauthentik/server:2021.5.1-rc1, | ||||||
|       - name: Push Docker Container to Registry (latest) |             ghcr.io/goauthentik/server:latest | ||||||
|         run: docker push beryju/authentik:latest |           platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 | ||||||
|   build-proxy: |   build-proxy: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
| @ -38,52 +42,66 @@ jobs: | |||||||
|           go get -u github.com/go-swagger/go-swagger/cmd/swagger |           go get -u github.com/go-swagger/go-swagger/cmd/swagger | ||||||
|           swagger generate client -f ../swagger.yaml -A authentik -t pkg/ |           swagger generate client -f ../swagger.yaml -A authentik -t pkg/ | ||||||
|           go build -v . |           go build -v . | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v1 | ||||||
|       - name: Docker Login Registry |       - name: Docker Login Registry | ||||||
|         env: |         uses: docker/login-action@v1 | ||||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} |         with: | ||||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} |           username: ${{ secrets.DOCKER_PASSWORD }} | ||||||
|         run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD |           password: ${{ secrets.DOCKER_USERNAME }} | ||||||
|       - name: Building Docker Image |       - name: Building Docker Image | ||||||
|         run: | |         uses: docker/build-push-action@v2 | ||||||
|           cd outpost/ |         with: | ||||||
|           docker build \ |           push: true | ||||||
|           --no-cache \ |           tags: | | ||||||
|           -t beryju/authentik-proxy:2021.4.1-rc2 \ |             beryju/authentik-proxy:2021.5.1-rc1, | ||||||
|           -t beryju/authentik-proxy:latest \ |             beryju/authentik-proxy:latest, | ||||||
|           -f proxy.Dockerfile . |             ghcr.io/goauthentik/proxy:2021.5.1-rc1, | ||||||
|       - name: Push Docker Container to Registry (versioned) |             ghcr.io/goauthentik/proxy:latest | ||||||
|         run: docker push beryju/authentik-proxy:2021.4.1-rc2 |           context: outpost/ | ||||||
|       - name: Push Docker Container to Registry (latest) |           file: outpost/proxy.Dockerfile | ||||||
|         run: docker push beryju/authentik-proxy:latest |           platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 | ||||||
|   build-static: |   build-ldap: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v1 |       - uses: actions/checkout@v1 | ||||||
|       - name: prepare ts api client |       - uses: actions/setup-go@v2 | ||||||
|  |         with: | ||||||
|  |           go-version: "^1.15" | ||||||
|  |       - name: prepare go api client | ||||||
|         run: | |         run: | | ||||||
|           docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0 |           cd outpost | ||||||
|  |           go get -u github.com/go-swagger/go-swagger/cmd/swagger | ||||||
|  |           swagger generate client -f ../swagger.yaml -A authentik -t pkg/ | ||||||
|  |           go build -v . | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v1 | ||||||
|       - name: Docker Login Registry |       - name: Docker Login Registry | ||||||
|         env: |         uses: docker/login-action@v1 | ||||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} |         with: | ||||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} |           username: ${{ secrets.DOCKER_PASSWORD }} | ||||||
|         run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD |           password: ${{ secrets.DOCKER_USERNAME }} | ||||||
|       - name: Building Docker Image |       - name: Building Docker Image | ||||||
|         run: | |         uses: docker/build-push-action@v2 | ||||||
|           cd web/ |         with: | ||||||
|           docker build \ |           push: true | ||||||
|           --no-cache \ |           tags: | | ||||||
|           -t beryju/authentik-static:2021.4.1-rc2 \ |             beryju/authentik-ldap:2021.5.1-rc1, | ||||||
|           -t beryju/authentik-static:latest \ |             beryju/authentik-ldap:latest, | ||||||
|           -f Dockerfile . |             ghcr.io/goauthentik/ldap:2021.5.1-rc1, | ||||||
|       - name: Push Docker Container to Registry (versioned) |             ghcr.io/goauthentik/ldap:latest | ||||||
|         run: docker push beryju/authentik-static:2021.4.1-rc2 |           context: outpost/ | ||||||
|       - name: Push Docker Container to Registry (latest) |           file: outpost/ldap.Dockerfile | ||||||
|         run: docker push beryju/authentik-static:latest |           platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 | ||||||
|   test-release: |   test-release: | ||||||
|     needs: |     needs: | ||||||
|       - build-server |       - build-server | ||||||
|       - build-static |  | ||||||
|       - build-proxy |       - build-proxy | ||||||
|  |       - build-ldap | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v1 |       - uses: actions/checkout@v1 | ||||||
| @ -103,12 +121,12 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v1 |       - uses: actions/checkout@v1 | ||||||
|       - name: Create a Sentry.io release |       - name: Create a Sentry.io release | ||||||
|         uses: tclindner/sentry-releases-action@v1.2.0 |         uses: getsentry/action-release@v1 | ||||||
|         env: |         env: | ||||||
|           SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} |           SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | ||||||
|           SENTRY_ORG: beryjuorg |           SENTRY_ORG: beryjuorg | ||||||
|           SENTRY_PROJECT: authentik |           SENTRY_PROJECT: authentik | ||||||
|           SENTRY_URL: https://sentry.beryju.org |           SENTRY_URL: https://sentry.beryju.org | ||||||
|         with: |         with: | ||||||
|           tagName: 2021.4.1-rc2 |           version: authentik@2021.5.1-rc1 | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								.github/workflows/tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -11,6 +11,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@master |       - uses: actions/checkout@master | ||||||
|  |       - name: prepare ts api client | ||||||
|  |         run: | | ||||||
|  |           docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0 | ||||||
|       - name: Pre-release test |       - name: Pre-release test | ||||||
|         run: | |         run: | | ||||||
|           sudo apt-get install -y pwgen |           sudo apt-get install -y pwgen | ||||||
| @ -25,15 +28,6 @@ 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 --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" |           docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" | ||||||
|       - name: Install Helm |  | ||||||
|         run: | |  | ||||||
|           apt update && apt install -y curl |  | ||||||
|           curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash |  | ||||||
|       - name: Helm package |  | ||||||
|         run: | |  | ||||||
|           helm dependency update helm/ |  | ||||||
|           helm package helm/ |  | ||||||
|           mv authentik-*.tgz authentik-chart.tgz |  | ||||||
|       - name: Extract version number |       - name: Extract version number | ||||||
|         id: get_version |         id: get_version | ||||||
|         uses: actions/github-script@0.2.0 |         uses: actions/github-script@0.2.0 | ||||||
| @ -51,13 +45,3 @@ jobs: | |||||||
|           release_name: Release ${{ steps.get_version.outputs.result }} |           release_name: Release ${{ steps.get_version.outputs.result }} | ||||||
|           draft: true |           draft: true | ||||||
|           prerelease: false |           prerelease: false | ||||||
|       - name: Upload packaged Helm Chart |  | ||||||
|         id: upload-release-asset |  | ||||||
|         uses: actions/upload-release-asset@v1.0.1 |  | ||||||
|         env: |  | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|         with: |  | ||||||
|           upload_url: ${{ steps.create_release.outputs.upload_url }} |  | ||||||
|           asset_path: ./authentik-chart.tgz |  | ||||||
|           asset_name: authentik-chart.tgz |  | ||||||
|           asset_content_type: application/gzip |  | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -202,3 +202,5 @@ selenium_screenshots/ | |||||||
| backups/ | backups/ | ||||||
| media/ | media/ | ||||||
| *mmdb | *mmdb | ||||||
|  |  | ||||||
|  | .idea/ | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,3 +1,4 @@ | |||||||
|  | # Stage 1: Lock python dependencies | ||||||
| FROM python:3.9-slim-buster as locker | FROM python:3.9-slim-buster as locker | ||||||
|  |  | ||||||
| COPY ./Pipfile /app/ | COPY ./Pipfile /app/ | ||||||
| @ -9,6 +10,34 @@ RUN pip install pipenv && \ | |||||||
|     pipenv lock -r > requirements.txt && \ |     pipenv lock -r > requirements.txt && \ | ||||||
|     pipenv lock -rd > requirements-dev.txt |     pipenv lock -rd > requirements-dev.txt | ||||||
|  |  | ||||||
|  | # Stage 2: Build webui | ||||||
|  | FROM node as npm-builder | ||||||
|  |  | ||||||
|  | COPY ./web /static/ | ||||||
|  |  | ||||||
|  | ENV NODE_ENV=production | ||||||
|  | RUN cd /static && npm i --production=false && npm run build | ||||||
|  |  | ||||||
|  | # Stage 3: Build go proxy | ||||||
|  | FROM golang:1.16.4 AS builder | ||||||
|  |  | ||||||
|  | WORKDIR /work | ||||||
|  |  | ||||||
|  | COPY --from=npm-builder /static/robots.txt /work/web/robots.txt | ||||||
|  | COPY --from=npm-builder /static/security.txt /work/web/security.txt | ||||||
|  | COPY --from=npm-builder /static/dist/ /work/web/dist/ | ||||||
|  | COPY --from=npm-builder /static/authentik/ /work/web/authentik/ | ||||||
|  |  | ||||||
|  | # RUN ls /work/web/static/authentik/ && exit 1 | ||||||
|  | COPY ./cmd /work/cmd | ||||||
|  | COPY ./web/static.go /work/web/static.go | ||||||
|  | COPY ./internal /work/internal | ||||||
|  | COPY ./go.mod /work/go.mod | ||||||
|  | COPY ./go.sum /work/go.sum | ||||||
|  |  | ||||||
|  | RUN go build -o /work/authentik ./cmd/server/main.go | ||||||
|  |  | ||||||
|  | # Stage 4: Run | ||||||
| FROM python:3.9-slim-buster | FROM python:3.9-slim-buster | ||||||
|  |  | ||||||
| WORKDIR / | WORKDIR / | ||||||
| @ -44,6 +73,7 @@ COPY ./pyproject.toml / | |||||||
| COPY ./xml /xml | COPY ./xml /xml | ||||||
| COPY ./manage.py / | COPY ./manage.py / | ||||||
| COPY ./lifecycle/ /lifecycle | COPY ./lifecycle/ /lifecycle | ||||||
|  | COPY --from=builder /work/authentik /authentik-proxy | ||||||
|  |  | ||||||
| USER authentik | USER authentik | ||||||
| STOPSIGNAL SIGINT | STOPSIGNAL SIGINT | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| all: lint-fix lint coverage gen | all: lint-fix lint test gen | ||||||
|  |  | ||||||
| test-integration: | test-integration: | ||||||
| 	k3d cluster create || exit 0 | 	k3d cluster create || exit 0 | ||||||
| @ -8,7 +8,7 @@ test-integration: | |||||||
| test-e2e: | test-e2e: | ||||||
| 	coverage run manage.py test --failfast -v 3 tests/e2e | 	coverage run manage.py test --failfast -v 3 tests/e2e | ||||||
|  |  | ||||||
| coverage: | test: | ||||||
| 	coverage run manage.py test -v 3 authentik | 	coverage run manage.py test -v 3 authentik | ||||||
| 	coverage html | 	coverage html | ||||||
| 	coverage report | 	coverage report | ||||||
| @ -22,7 +22,7 @@ lint: | |||||||
| 	bandit -r authentik tests lifecycle -x node_modules | 	bandit -r authentik tests lifecycle -x node_modules | ||||||
| 	pylint authentik tests lifecycle | 	pylint authentik tests lifecycle | ||||||
|  |  | ||||||
| gen: coverage | gen: | ||||||
| 	./manage.py generate_swagger -o swagger.yaml -f yaml | 	./manage.py generate_swagger -o swagger.yaml -f yaml | ||||||
|  |  | ||||||
| local-stack: | local-stack: | ||||||
| @ -31,7 +31,5 @@ local-stack: | |||||||
| 	docker-compose up -d | 	docker-compose up -d | ||||||
| 	docker-compose run --rm server migrate | 	docker-compose run --rm server migrate | ||||||
|  |  | ||||||
| build-static: | run: | ||||||
| 	docker-compose -f scripts/ci.docker-compose.yml up -d | 	go run -v cmd/server/main.go | ||||||
| 	docker build -t beryju/authentik-static -f static.Dockerfile --network=scripts_default . |  | ||||||
| 	docker-compose -f scripts/ci.docker-compose.yml down -v |  | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Pipfile
									
									
									
									
									
								
							| @ -32,7 +32,7 @@ lxml = ">=4.6.3" | |||||||
| packaging = "*" | packaging = "*" | ||||||
| psycopg2-binary = "*" | psycopg2-binary = "*" | ||||||
| pycryptodome = "*" | pycryptodome = "*" | ||||||
| pyjwkest = "*" | pyjwt = "*" | ||||||
| pyyaml = "*" | pyyaml = "*" | ||||||
| requests-oauthlib = "*" | requests-oauthlib = "*" | ||||||
| sentry-sdk = "*" | sentry-sdk = "*" | ||||||
| @ -59,3 +59,4 @@ pylint-django = "*" | |||||||
| pytest = "*" | pytest = "*" | ||||||
| pytest-django = "*" | pytest-django = "*" | ||||||
| selenium = "*" | selenium = "*" | ||||||
|  | requests-mock = "*" | ||||||
|  | |||||||
							
								
								
									
										479
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										479
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "a9d504f00ee8820017f26a4fda2938de456cb72b4bc2f8735fc8c6a6c615d46a" |             "sha256": "17be2923cf8d281e430ec1467aea723806ac6f7c58fc6553ede92317e43f4d14" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @ -56,7 +56,6 @@ | |||||||
|                 "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", |                 "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", | ||||||
|                 "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" |                 "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==3.7.4.post0" |             "version": "==3.7.4.post0" | ||||||
|         }, |         }, | ||||||
|         "aioredis": { |         "aioredis": { | ||||||
| @ -71,7 +70,6 @@ | |||||||
|                 "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", |                 "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", | ||||||
|                 "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" |                 "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==5.0.6" |             "version": "==5.0.6" | ||||||
|         }, |         }, | ||||||
|         "asgiref": { |         "asgiref": { | ||||||
| @ -79,7 +77,6 @@ | |||||||
|                 "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", |                 "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", | ||||||
|                 "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" |                 "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==3.3.4" |             "version": "==3.3.4" | ||||||
|         }, |         }, | ||||||
|         "async-timeout": { |         "async-timeout": { | ||||||
| @ -87,23 +84,20 @@ | |||||||
|                 "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", |                 "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", | ||||||
|                 "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" |                 "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_full_version >= '3.5.3'", |  | ||||||
|             "version": "==3.0.1" |             "version": "==3.0.1" | ||||||
|         }, |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", |                 "sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8", | ||||||
|                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" |                 "sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "version": "==21.1.0" | ||||||
|             "version": "==20.3.0" |  | ||||||
|         }, |         }, | ||||||
|         "autobahn": { |         "autobahn": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac", |                 "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac", | ||||||
|                 "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03" |                 "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.7'", |  | ||||||
|             "version": "==21.3.1" |             "version": "==21.3.1" | ||||||
|         }, |         }, | ||||||
|         "automat": { |         "automat": { | ||||||
| @ -122,29 +116,25 @@ | |||||||
|         }, |         }, | ||||||
|         "boto3": { |         "boto3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:d5ef160442925f5944e4cde88589f0f195f6c284f05613114fc6bbc35e342fa7", |                 "sha256:56f1766f1271b6b4e979c7b56225377f8912050e5935adc5c1c9e3a0338b949e", | ||||||
|                 "sha256:df5912350e092e795f72d8047a44d3f5af9690317acfe48147b9853a2f89b304", |                 "sha256:c61c809d288e88b9a0d926f56f803d0128b498aa9b45a42a6e03cd9a83e5c124" | ||||||
|                 "sha256:a482135c30fa07eaf4370314dd0fb49117222a266d0423b2075aed3835ed1f04", |  | ||||||
|                 "sha256:e86c15049dc07cb67e8b466795f004f1f23c1acf078d47283cd5e4a692a5aa37" |  | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.17.49" |             "version": "==1.17.68" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6a672ba41dd00e5c1c1824ca8143d180d88de8736d78c0b1f96b8d3cb0466561", |                 "sha256:0f693f5ad6348ec1a62b3a66fee2840d3b722d66b44896022d644275ff8b143d", | ||||||
|                 "sha256:f7f103fa0651c69dd360c7d0ecd874854303de5cc0869e0cbc2818a52baacc69" |                 "sha256:eb3544911cb0316a33b328a27d137130af278a9c0006be0c95e5e402b01d9865" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", |             "version": "==1.20.68" | ||||||
|             "version": "==1.20.49" |  | ||||||
|         }, |         }, | ||||||
|         "cachetools": { |         "cachetools": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2", |                 "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", | ||||||
|                 "sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9" |                 "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version ~= '3.5'", |             "version": "==4.2.2" | ||||||
|             "version": "==4.2.1" |  | ||||||
|         }, |         }, | ||||||
|         "cbor2": { |         "cbor2": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -230,7 +220,6 @@ | |||||||
|                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", |                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", | ||||||
|                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" |                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==4.0.0" |             "version": "==4.0.0" | ||||||
|         }, |         }, | ||||||
|         "click": { |         "click": { | ||||||
| @ -238,7 +227,6 @@ | |||||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", |                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" |                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==7.1.2" |             "version": "==7.1.2" | ||||||
|         }, |         }, | ||||||
|         "click-didyoumean": { |         "click-didyoumean": { | ||||||
| @ -312,7 +300,6 @@ | |||||||
|                 "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", |                 "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", | ||||||
|                 "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" |                 "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==3.0.2" |             "version": "==3.0.2" | ||||||
|         }, |         }, | ||||||
|         "defusedxml": { |         "defusedxml": { | ||||||
| @ -325,11 +312,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django": { |         "django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927", |                 "sha256:0a1d195ad65c52bf275b8277b3d49680bd1137a5f55039a806f25f6b9752ce3d", | ||||||
|                 "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d" |                 "sha256:18dd3145ddbd04bf189ff79b9954d08fda5171ea7b57bf705789fea766a07d50" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.2" |             "version": "==3.2.2" | ||||||
|         }, |         }, | ||||||
|         "django-dbbackup": { |         "django-dbbackup": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -364,11 +351,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django-otp": { |         "django-otp": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:381a15e65293b8b06d47b7d6b306e0b7af2e104137ac92f6c566d3b9b90b6244", |                 "sha256:04852c5301befb02d1d8ba4a31d375eb08d7c2cb6fe86b5f840867435ab1309c", | ||||||
|                 "sha256:f4ab096b424c33ffe69453620356e1b7517f30dfb9ba13bfeaa1d1f20faddc13" |                 "sha256:3916fc7652c2f934b1cf3807dd8ed257ce7605c10dfefa27fadda5628d9a9c9e" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.0.3" |             "version": "==1.0.4" | ||||||
|         }, |         }, | ||||||
|         "django-prometheus": { |         "django-prometheus": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -438,7 +425,6 @@ | |||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" |                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==0.18.2" |             "version": "==0.18.2" | ||||||
|         }, |         }, | ||||||
|         "geoip2": { |         "geoip2": { | ||||||
| @ -451,14 +437,14 @@ | |||||||
|         }, |         }, | ||||||
|         "google-auth": { |         "google-auth": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:186fe2564634d67fbbb64f3daf8bc8c9cecbb2a7f535ed1a8a71795e50db8d87", |                 "sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f", | ||||||
|                 "sha256:70b39558712826e41f65e5f05a8d879361deaf84df8883e5dd0ec3d0da6ab66e" |                 "sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", |             "version": "==1.30.0" | ||||||
|             "version": "==1.28.1" |  | ||||||
|         }, |         }, | ||||||
|         "gunicorn": { |         "gunicorn": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  |                 "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", | ||||||
|                 "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" |                 "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
| @ -469,7 +455,6 @@ | |||||||
|                 "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", |                 "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", | ||||||
|                 "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" |                 "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==0.12.0" |             "version": "==0.12.0" | ||||||
|         }, |         }, | ||||||
|         "hiredis": { |         "hiredis": { | ||||||
| @ -516,25 +501,27 @@ | |||||||
|                 "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", |                 "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", | ||||||
|                 "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" |                 "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==2.0.0" |             "version": "==2.0.0" | ||||||
|         }, |         }, | ||||||
|         "httptools": { |         "httptools": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", |                 "sha256:07659649fe6b3948b6490825f89abe5eb1cec79ebfaaa0b4bf30f3f33f3c2ba8", | ||||||
|                 "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", |                 "sha256:08b79e09114e6ab5c3dbf560bba2cb2257ea38cdaeaf99b7cb80d8f92622fcd9", | ||||||
|                 "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", |                 "sha256:1e35aa179b67086cc600a984924a88589b90793c9c1b260152ca4908786e09df", | ||||||
|                 "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", |                 "sha256:31629e1f1b89959f8c0927bad12184dc07977dcf71e24f4772934aa490aa199b", | ||||||
|                 "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", |                 "sha256:851026bd63ec0af7e7592890d97d15c92b62d9e17094353f19a52c8e2b33710a", | ||||||
|                 "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", |                 "sha256:8fcca4b7efe353b13a24017211334c57d055a6e132c7adffed13a10d28efca57", | ||||||
|                 "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", |                 "sha256:9abd788465aa46a0f288bd3a99e53edd184177d6379e2098fd6097bb359ad9d6", | ||||||
|                 "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", |                 "sha256:aebdf0bd7bf7c90ae6b3be458692bf6e9e5b610b501f9f74c7979015a51db4c4", | ||||||
|                 "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", |                 "sha256:bda99a5723e7eab355ce57435c70853fc137a65aebf2f1cd4d15d96e2956da7b", | ||||||
|                 "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", |                 "sha256:c1c63d860749841024951b0a78e4dec6f543d23751ef061d6ab60064c7b8b524", | ||||||
|                 "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", |                 "sha256:c4111a0a8a00eff1e495d43ea5230aaf64968a48ddba8ea2d5f982efae827404", | ||||||
|                 "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" |                 "sha256:dce59ee45dd6ee6c434346a5ac527c44014326f560866b4b2f414a692ee1aca8", | ||||||
|  |                 "sha256:f759717ca1b2ef498c67ba4169c2b33eecf943a89f5329abcff8b89d153eb500", | ||||||
|  |                 "sha256:fb7199b8fb0c50a22e77260bb59017e0c075fa80cb03bb2c8692de76e7bb7fe7", | ||||||
|  |                 "sha256:fbf7ecd31c39728f251b1c095fd27c84e4d21f60a1d079a0333472ff3ae59d34" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.1.1" |             "version": "==0.1.2" | ||||||
|         }, |         }, | ||||||
|         "hyperlink": { |         "hyperlink": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -562,7 +549,6 @@ | |||||||
|                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", |                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", | ||||||
|                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" |                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.5'", |  | ||||||
|             "version": "==0.5.1" |             "version": "==0.5.1" | ||||||
|         }, |         }, | ||||||
|         "itypes": { |         "itypes": { | ||||||
| @ -577,7 +563,6 @@ | |||||||
|                 "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", |                 "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", | ||||||
|                 "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" |                 "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==2.11.3" |             "version": "==2.11.3" | ||||||
|         }, |         }, | ||||||
|         "jmespath": { |         "jmespath": { | ||||||
| @ -585,7 +570,6 @@ | |||||||
|                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", |                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", | ||||||
|                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" |                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==0.10.0" |             "version": "==0.10.0" | ||||||
|         }, |         }, | ||||||
|         "jsonschema": { |         "jsonschema": { | ||||||
| @ -600,7 +584,6 @@ | |||||||
|                 "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", |                 "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", | ||||||
|                 "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" |                 "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==5.0.2" |             "version": "==5.0.2" | ||||||
|         }, |         }, | ||||||
|         "kubernetes": { |         "kubernetes": { | ||||||
| @ -613,9 +596,6 @@ | |||||||
|         }, |         }, | ||||||
|         "ldap3": { |         "ldap3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056", |  | ||||||
|                 "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c", |  | ||||||
|                 "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59", |  | ||||||
|                 "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", |                 "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", | ||||||
|                 "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" |                 "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" | ||||||
|             ], |             ], | ||||||
| @ -627,18 +607,24 @@ | |||||||
|                 "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", |                 "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", | ||||||
|                 "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", |                 "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", | ||||||
|                 "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", |                 "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", | ||||||
|  |                 "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae", | ||||||
|                 "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", |                 "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", | ||||||
|                 "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", |                 "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", | ||||||
|                 "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", |                 "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", | ||||||
|                 "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", |                 "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", | ||||||
|  |                 "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59", | ||||||
|                 "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", |                 "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", | ||||||
|                 "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", |                 "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", | ||||||
|  |                 "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96", | ||||||
|                 "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", |                 "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", | ||||||
|                 "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", |                 "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", | ||||||
|                 "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", |                 "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", | ||||||
|  |                 "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354", | ||||||
|                 "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", |                 "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", | ||||||
|                 "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", |                 "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", | ||||||
|  |                 "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16", | ||||||
|                 "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", |                 "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", | ||||||
|  |                 "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a", | ||||||
|                 "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", |                 "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", | ||||||
|                 "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", |                 "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", | ||||||
|                 "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", |                 "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", | ||||||
| @ -651,10 +637,14 @@ | |||||||
|                 "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", |                 "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", | ||||||
|                 "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", |                 "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", | ||||||
|                 "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", |                 "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", | ||||||
|  |                 "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617", | ||||||
|                 "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", |                 "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", | ||||||
|  |                 "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92", | ||||||
|                 "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", |                 "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", | ||||||
|                 "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", |                 "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", | ||||||
|  |                 "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24", | ||||||
|                 "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", |                 "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", | ||||||
|  |                 "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e", | ||||||
|                 "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", |                 "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", | ||||||
|                 "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", |                 "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", | ||||||
|                 "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", |                 "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", | ||||||
| @ -719,14 +709,12 @@ | |||||||
|                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", |                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", | ||||||
|                 "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" |                 "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==1.1.1" |             "version": "==1.1.1" | ||||||
|         }, |         }, | ||||||
|         "maxminddb": { |         "maxminddb": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" |                 "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==2.0.3" |             "version": "==2.0.3" | ||||||
|         }, |         }, | ||||||
|         "msgpack": { |         "msgpack": { | ||||||
| @ -802,7 +790,6 @@ | |||||||
|                 "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", |                 "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", | ||||||
|                 "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" |                 "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==5.1.0" |             "version": "==5.1.0" | ||||||
|         }, |         }, | ||||||
|         "oauthlib": { |         "oauthlib": { | ||||||
| @ -810,7 +797,6 @@ | |||||||
|                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", |                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", | ||||||
|                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" |                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==3.1.0" |             "version": "==3.1.0" | ||||||
|         }, |         }, | ||||||
|         "packaging": { |         "packaging": { | ||||||
| @ -826,7 +812,6 @@ | |||||||
|                 "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa", |                 "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa", | ||||||
|                 "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d" |                 "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==0.10.1" |             "version": "==0.10.1" | ||||||
|         }, |         }, | ||||||
|         "prompt-toolkit": { |         "prompt-toolkit": { | ||||||
| @ -834,7 +819,6 @@ | |||||||
|                 "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", |                 "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", | ||||||
|                 "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" |                 "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_full_version >= '3.6.1'", |  | ||||||
|             "version": "==3.0.18" |             "version": "==3.0.18" | ||||||
|         }, |         }, | ||||||
|         "psycopg2-binary": { |         "psycopg2-binary": { | ||||||
| @ -880,37 +864,15 @@ | |||||||
|         }, |         }, | ||||||
|         "pyasn1": { |         "pyasn1": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", |                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", | ||||||
|                 "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", |                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" | ||||||
|                 "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", |  | ||||||
|                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", |  | ||||||
|                 "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", |  | ||||||
|                 "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", |  | ||||||
|                 "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", |  | ||||||
|                 "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", |  | ||||||
|                 "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", |  | ||||||
|                 "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", |  | ||||||
|                 "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3", |  | ||||||
|                 "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", |  | ||||||
|                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d" |  | ||||||
|             ], |             ], | ||||||
|             "version": "==0.4.8" |             "version": "==0.4.8" | ||||||
|         }, |         }, | ||||||
|         "pyasn1-modules": { |         "pyasn1-modules": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", |  | ||||||
|                 "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", |  | ||||||
|                 "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", |  | ||||||
|                 "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405", |  | ||||||
|                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", |  | ||||||
|                 "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", |  | ||||||
|                 "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", |  | ||||||
|                 "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", |  | ||||||
|                 "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", |  | ||||||
|                 "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", |  | ||||||
|                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", |                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", | ||||||
|                 "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", |                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" | ||||||
|                 "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd" |  | ||||||
|             ], |             ], | ||||||
|             "version": "==0.2.8" |             "version": "==0.2.8" | ||||||
|         }, |         }, | ||||||
| @ -919,7 +881,6 @@ | |||||||
|                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", |                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", | ||||||
|                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" |                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==2.20" |             "version": "==2.20" | ||||||
|         }, |         }, | ||||||
|         "pycryptodome": { |         "pycryptodome": { | ||||||
| @ -958,56 +919,20 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.10.1" |             "version": "==3.10.1" | ||||||
|         }, |         }, | ||||||
|         "pycryptodomex": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:00a584ee52bf5e27d540129ca9bf7c4a7e7447f24ff4a220faa1304ad0c09bcd", |  | ||||||
|                 "sha256:04265a7a84ae002001249bd1de2823bcf46832bd4b58f6965567cb8a07cf4f00", |  | ||||||
|                 "sha256:0bd35af6a18b724c689e56f2dbbdd8e409288be71952d271ba3d9614b31d188c", |  | ||||||
|                 "sha256:20c45a30f3389148f94edb77f3b216c677a277942f62a2b81a1cc0b6b2dde7fc", |  | ||||||
|                 "sha256:2959304d1ce31ab303d9fb5db2b294814278b35154d9b30bf7facc52d6088d0a", |  | ||||||
|                 "sha256:36dab7f506948056ceba2d57c1ade74e898401960de697cefc02f3519bd26c1b", |  | ||||||
|                 "sha256:37ec1b407ec032c7a0c1fdd2da12813f560bad38ae61ad9c7ce3c0573b3e5e30", |  | ||||||
|                 "sha256:3b8eb85b3cc7f083d87978c264d10ff9de3b4bfc46f1c6fdc2792e7d7ebc87bb", |  | ||||||
|                 "sha256:3dfce70c4e425607ae87b8eae67c9c7dbba59a33b62d70f79417aef0bc5c735b", |  | ||||||
|                 "sha256:418f51c61eab52d9920f4ef468d22c89dab1be5ac796f71cf3802f6a6e667df0", |  | ||||||
|                 "sha256:4195604f75cdc1db9bccdb9e44d783add3c817319c30aaff011670c9ed167690", |  | ||||||
|                 "sha256:4344ab16faf6c2d9df2b6772995623698fb2d5f114dace4ab2ff335550cf71d5", |  | ||||||
|                 "sha256:541cd3e3e252fb19a7b48f420b798b53483302b7fe4d9954c947605d0a263d62", |  | ||||||
|                 "sha256:564063e3782474c92cbb333effd06e6eb718471783c6e67f28c63f0fc3ac7b23", |  | ||||||
|                 "sha256:72f44b5be46faef2a1bf2a85902511b31f4dd7b01ce0c3978e92edb2cc812a82", |  | ||||||
|                 "sha256:8a98e02cbf8f624add45deff444539bf26345b479fc04fa0937b23cd84078d91", |  | ||||||
|                 "sha256:940db96449d7b2ebb2c7bf190be1514f3d67914bd37e54e8d30a182bd375a1a9", |  | ||||||
|                 "sha256:961333e7ee896651f02d4692242aa36b787b8e8e0baa2256717b2b9d55ae0a3c", |  | ||||||
|                 "sha256:9f713ffb4e27b5575bd917c70bbc3f7b348241a351015dbbc514c01b7061ff7e", |  | ||||||
|                 "sha256:a6584ae58001d17bb4dc0faa8a426919c2c028ef4d90ceb4191802ca6edb8204", |  | ||||||
|                 "sha256:c2b680987f418858e89dbb4f09c8c919ece62811780a27051ace72b2f69fb1be", |  | ||||||
|                 "sha256:d8fae5ba3d34c868ae43614e0bd6fb61114b2687ac3255798791ce075d95aece", |  | ||||||
|                 "sha256:dbd2c361db939a4252589baa94da4404d45e3fc70da1a31e541644cdf354336e", |  | ||||||
|                 "sha256:e090a8609e2095aa86978559b140cf8968af99ee54b8791b29ff804838f29f10", |  | ||||||
|                 "sha256:e4a1245e7b846e88ba63e7543483bda61b9acbaee61eadbead5a1ce479d94740", |  | ||||||
|                 "sha256:ec9901d19cadb80d9235ee41cc58983f18660314a0eb3fc7b11b0522ac3b6c4a", |  | ||||||
|                 "sha256:f2abeb4c4ce7584912f4d637b2c57f23720d35dd2892bfeb1b2c84b6fb7a8c88", |  | ||||||
|                 "sha256:f3bb267df679f70a9f40f17d62d22fe12e8b75e490f41807e7560de4d3e6bf9f", |  | ||||||
|                 "sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34", |  | ||||||
|                 "sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38" |  | ||||||
|             ], |  | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==3.10.1" |  | ||||||
|         }, |  | ||||||
|         "pyhamcrest": { |         "pyhamcrest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", |                 "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", | ||||||
|                 "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" |                 "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.5'", |  | ||||||
|             "version": "==2.0.2" |             "version": "==2.0.2" | ||||||
|         }, |         }, | ||||||
|         "pyjwkest": { |         "pyjwt": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222" |                 "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", | ||||||
|  |                 "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.4.2" |             "version": "==2.1.0" | ||||||
|         }, |         }, | ||||||
|         "pyopenssl": { |         "pyopenssl": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1021,14 +946,12 @@ | |||||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", |                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" |                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==2.4.7" |             "version": "==2.4.7" | ||||||
|         }, |         }, | ||||||
|         "pyrsistent": { |         "pyrsistent": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" |                 "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.5'", |  | ||||||
|             "version": "==0.17.3" |             "version": "==0.17.3" | ||||||
|         }, |         }, | ||||||
|         "python-dateutil": { |         "python-dateutil": { | ||||||
| @ -1036,15 +959,14 @@ | |||||||
|                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", |                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", | ||||||
|                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" |                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==2.8.1" |             "version": "==2.8.1" | ||||||
|         }, |         }, | ||||||
|         "python-dotenv": { |         "python-dotenv": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a", |                 "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544", | ||||||
|                 "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2" |                 "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.17.0" |             "version": "==0.17.1" | ||||||
|         }, |         }, | ||||||
|         "pytz": { |         "pytz": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1093,7 +1015,6 @@ | |||||||
|                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", |                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", | ||||||
|                 "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" |                 "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==3.5.3" |             "version": "==3.5.3" | ||||||
|         }, |         }, | ||||||
|         "requests": { |         "requests": { | ||||||
| @ -1101,13 +1022,11 @@ | |||||||
|                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", |                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", | ||||||
|                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" |                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==2.25.1" |             "version": "==2.25.1" | ||||||
|         }, |         }, | ||||||
|         "requests-oauthlib": { |         "requests-oauthlib": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", |                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", | ||||||
|                 "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc", |  | ||||||
|                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" |                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
| @ -1126,7 +1045,6 @@ | |||||||
|                 "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28", |                 "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28", | ||||||
|                 "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22" |                 "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3'", |  | ||||||
|             "version": "==0.17.4" |             "version": "==0.17.4" | ||||||
|         }, |         }, | ||||||
|         "ruamel.yaml.clib": { |         "ruamel.yaml.clib": { | ||||||
| @ -1163,23 +1081,23 @@ | |||||||
|                 "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", |                 "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", | ||||||
|                 "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" |                 "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version < '3.10' and platform_python_implementation == 'CPython'", |             "markers": "platform_python_implementation == 'CPython' and python_version < '3.10'", | ||||||
|             "version": "==0.2.2" |             "version": "==0.2.2" | ||||||
|         }, |         }, | ||||||
|         "s3transfer": { |         "s3transfer": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2", |                 "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc", | ||||||
|                 "sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547" |                 "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.3.6" |             "version": "==0.4.2" | ||||||
|         }, |         }, | ||||||
|         "sentry-sdk": { |         "sentry-sdk": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:71de00c9711926816f750bc0f57ef2abbcb1bfbdf5378c601df7ec978f44857a", |                 "sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739", | ||||||
|                 "sha256:9221e985f425913204989d0e0e1cbb719e8b7fa10540f1bc509f660c06a34e66" |                 "sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.0.0" |             "version": "==1.1.0" | ||||||
|         }, |         }, | ||||||
|         "service-identity": { |         "service-identity": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1191,18 +1109,16 @@ | |||||||
|         }, |         }, | ||||||
|         "six": { |         "six": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", |                 "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", | ||||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" |                 "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "version": "==1.16.0" | ||||||
|             "version": "==1.15.0" |  | ||||||
|         }, |         }, | ||||||
|         "sqlparse": { |         "sqlparse": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", |                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", | ||||||
|                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" |                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.5'", |  | ||||||
|             "version": "==0.4.1" |             "version": "==0.4.1" | ||||||
|         }, |         }, | ||||||
|         "structlog": { |         "structlog": { | ||||||
| @ -1258,23 +1174,21 @@ | |||||||
|                 "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8", |                 "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8", | ||||||
|                 "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb" |                 "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==21.2.1" |             "version": "==21.2.1" | ||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |         "typing-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", |                 "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", | ||||||
|                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", |                 "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", | ||||||
|                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" |                 "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.7.4.3" |             "version": "==3.10.0.0" | ||||||
|         }, |         }, | ||||||
|         "uritemplate": { |         "uritemplate": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", |                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", | ||||||
|                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" |                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==3.0.1" |             "version": "==3.0.1" | ||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
| @ -1319,7 +1233,6 @@ | |||||||
|                 "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", |                 "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", | ||||||
|                 "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" |                 "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==5.0.0" |             "version": "==5.0.0" | ||||||
|         }, |         }, | ||||||
|         "watchgod": { |         "watchgod": { | ||||||
| @ -1346,11 +1259,10 @@ | |||||||
|         }, |         }, | ||||||
|         "websocket-client": { |         "websocket-client": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663", |                 "sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32", | ||||||
|                 "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f" |                 "sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "version": "==0.59.0" | ||||||
|             "version": "==0.58.0" |  | ||||||
|         }, |         }, | ||||||
|         "websockets": { |         "websockets": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1381,22 +1293,20 @@ | |||||||
|         }, |         }, | ||||||
|         "xmlsec": { |         "xmlsec": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:252f79ed4482d6eefcca62c3bfc99b8d95c07abd846262d854a207ec4d67fac5", |                 "sha256:17d2e66d4e3e601d210eed936b53c3eb44cddaef62f60b5c6ad5c18e948d926c", | ||||||
|                 "sha256:31884dc97cc34cf1681a0f239f613969e61f9a01f4c2d2a62e53d68216fe42d6", |                 "sha256:2bc1b871b49d6580779805a4a1c2d835e834a2fa614fe40cf71931d11a8279cf", | ||||||
|                 "sha256:32a669dfe447bccecdb4ef79221c0452ce6dad919f3a75daf512792141a54dac", |                 "sha256:52eded125c0d1ab72125105ef061370c6b06ab9bd37e29a61bc2f8a61205bae4", | ||||||
|                 "sha256:3d13d7b6cb921dbc4d60d00ad00081a038df73a1e69f5bcc3695deb1bf2093b0", |                 "sha256:72af9a5a747a5fe6e425d2be10daa43d18307dbe03498df3820fc3cd93daa148", | ||||||
|                 "sha256:5e2f263a21fd146859911479ec35e40a57f519e650f56c775f91367d2a1b6e15", |                 "sha256:806855d505da24aeb77758a6f373b1473e5ed63bdbe346af90cc6d2b053e4716", | ||||||
|                 "sha256:61076be98da4c7cf842a78aa3f129a5039f2ba4992e02480eefe78028d317698", |                 "sha256:8746dd992aaec06ed8ff1615f4a8e2a32258e8af38f9a9f8acf3ee1fb34a5da6", | ||||||
|                 "sha256:69d7f965d6b74b3266f7baa99a0377d9c76acbf26c615b4ee8d2cbe17bf85528", |                 "sha256:9d52b2b15d42292725e4f9d8a5b040e39cba0a9cd58059ac951e7310d6340bb9", | ||||||
|                 "sha256:6d8bb24c3a4db398011f394e29b58cd34c9c26d76b772c5d418d8579df127234", |                 "sha256:b380f3ebc042f71afab057632481d06e06f1ba4f90047d91ca92612a7d3d487b", | ||||||
|                 "sha256:6d9d46d1f6b4985023469a1e334cb35c7c8fc6bd9d8b65ca52b923a7a6869c2a", |                 "sha256:be0f475edd8e9c98f57449c97839f6a81946e79e4cccb81e4b5196a2cc40e044", | ||||||
|                 "sha256:8a7ffdc4f7f760253aa4dd8d2037358eb33915ca1dcf1c2422b19fcf0ab68506", |                 "sha256:bf3c62d154f2222caf56d897ddfd53fd0aef560d5a2202447d90e015301a0a10", | ||||||
|                 "sha256:927fc5755bb93dc09275bd5d818811e016290c194012d63f8e6f86b7ece3e468", |                 "sha256:fe6a5f05aba3ff47e105a308482b68f8b0fd80656eb1456a9c1e4de47d2c580f" | ||||||
|                 "sha256:dcaa084c3700f775eba09d81a1432444f82d9ad6270320c56c1a733d71cceb3a", |  | ||||||
|                 "sha256:f59698cc0366395ca79b48b080674973541aae290670c57d88f05d939a4c00da" |  | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.3.9" |             "version": "==1.3.10" | ||||||
|         }, |         }, | ||||||
|         "yarl": { |         "yarl": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1438,65 +1348,63 @@ | |||||||
|                 "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", |                 "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", | ||||||
|                 "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" |                 "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==1.6.3" |             "version": "==1.6.3" | ||||||
|         }, |         }, | ||||||
|         "zope.interface": { |         "zope.interface": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:02d3535aa18e34ce97c58d241120b7554f7d1cf4f8002fc9675cc7e7745d20e8", |                 "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", | ||||||
|                 "sha256:0378a42ec284b65706d9ef867600a4a31701a0d6773434e6537cfc744e3343f4", |                 "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", | ||||||
|                 "sha256:07d289358a8c565ea09e426590dd1179f93cf5ac3dd17d43fcc4fc63c1a9d275", |                 "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", | ||||||
|                 "sha256:0e6cdbdd94ae94d1433ab51f46a76df0f2cd041747c31baec1c1ffa4e76bd0c1", |                 "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", | ||||||
|                 "sha256:11354fb8b8bdc5cdd66358ed4f1f0ce739d78ff6d215d33b8f3ae282258c0f11", |                 "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", | ||||||
|                 "sha256:12588a46ae0a99f172c4524cbbc3bb870f32e0f8405e9fa11a5ef3fa3a808ad7", |                 "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", | ||||||
|                 "sha256:16caa44a06f6b0b2f7626ced4b193c1ae5d09c1b49c9b4962c93ae8aa2134f55", |                 "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", | ||||||
|                 "sha256:18c478b89b6505756f007dcf76a67224a23dcf0f365427742ed0c0473099caa4", |                 "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", | ||||||
|                 "sha256:221b41442cf4428fcda7fc958c9721c916709e2a3a9f584edd70f1493a09a762", |                 "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", | ||||||
|                 "sha256:26109c50ccbcc10f651f76277cfc05fba8418a907daccc300c9247f24b3158a2", |                 "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", | ||||||
|                 "sha256:28d8157f8c77662a1e0796a7d3cfa8910289131d4b4dd4e10b2686ab1309b67b", |                 "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", | ||||||
|                 "sha256:2c51689b7b40c7d9c7e8a678350e73dc647945a13b4e416e7a02bbf0c37bdb01", |                 "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", | ||||||
|                 "sha256:2ec58e1e1691dde4fbbd97f8610de0f8f1b1a38593653f7d3b8e931b9cd6d67f", |                 "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", | ||||||
|                 "sha256:416feb6500f7b6fc00d32271f6b8495e67188cb5eb51fc8e289b81fdf465a9cb", |                 "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", | ||||||
|                 "sha256:520352b18adea5478bbf387e9c77910a914985671fe36bc5ef19fdcb67a854bc", |                 "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", | ||||||
|                 "sha256:527415b5ca201b4add44026f70278fbc0b942cf0801a26ca5527cb0389b6151e", |                 "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", | ||||||
|                 "sha256:54243053316b5eec92affe43bbace7c8cd946bc0974a4aa39ff1371df0677b22", |                 "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", | ||||||
|                 "sha256:61b8454190b9cc87279232b6de28dee0bad040df879064bb2f0e505cda907918", |                 "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", | ||||||
|                 "sha256:672668729edcba0f2ee522ab177fcad91c81cfce991c24d8767765e2637d3515", |                 "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", | ||||||
|                 "sha256:67aa26097e194947d29f2b5a123830e03da1519bcce10cac034a51fcdb99c34f", |                 "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", | ||||||
|                 "sha256:6e7305e42b5f54e5ccf51820de46f0a7c951ba7cb9e3f519e908545b0f5628d0", |                 "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", | ||||||
|                 "sha256:7234ac6782ca43617de803735949f79b894f0c5d353fbc001d745503c69e6d1d", |                 "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", | ||||||
|                 "sha256:7426bea25bdf92f00fa52c7b30fcd2a2f71c21cf007178971b1f248b6c2d3145", |                 "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", | ||||||
|                 "sha256:74b331c5d5efdddf5bbd9e1f7d8cb91a0d6b9c4ba45ca3e9003047a84dca1a3b", |                 "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", | ||||||
|                 "sha256:79b6db1a18253db86e9bf1e99fa829d60fd3fc7ac04f4451c44e4bdcf6756a42", |                 "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", | ||||||
|                 "sha256:7d79cd354ae0a033ac7b86a2889c9e8bb0bb48243a6ed27fc5064ce49b842ada", |                 "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", | ||||||
|                 "sha256:823d1b4a6a028b8327e64865e2c81a8959ae9f4e7c9c8e0eec814f4f9b36b362", |                 "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", | ||||||
|                 "sha256:8715717a5861932b7fe7f3cbd498c82ff4132763e2fea182cc95e53850394ec1", |                 "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", | ||||||
|                 "sha256:89a6091f2d07936c8a96ce56f2000ecbef20fb420a94845e7d53913c558a6378", |                 "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", | ||||||
|                 "sha256:8af4b3116e4a37059bc8c7fe36d4a73d7c1d8802a1d8b6e549f1380d13a40160", |                 "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", | ||||||
|                 "sha256:8b4b0034e6c7f30133fa64a1cc276f8f1a155ef9529e7eb93a3c1728b40c0f5c", |                 "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", | ||||||
|                 "sha256:92195df3913c1de80062635bf64cd7bd0d0934a7fa1689b6d287d1cbbd16922c", |                 "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", | ||||||
|                 "sha256:96c2e68385f3848d58f19b2975a675532abdb65c8fa5f04d94b95b27b6b1ffa7", |                 "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", | ||||||
|                 "sha256:9c7044dbbf8c58420a9ef4ed6901f5a8b7698d90cd984d7f57a18c78474686f6", |                 "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", | ||||||
|                 "sha256:a1937efed7e3fe0ee74630e1960df887d8aa83c571e1cf4db9d15b9c181d457d", |                 "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", | ||||||
|                 "sha256:a38c10423a475a1658e2cb8f52cf84ec20a4c0adff724dd43a6b45183f499bc1", |                 "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", | ||||||
|                 "sha256:a413c424199bcbab71bf5fa7538246f27177fbd6dd74b2d9c5f34878658807f8", |                 "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", | ||||||
|                 "sha256:b18a855f8504743e0a2d8b75d008c7720d44e4c76687e13f959e35d9a13eb397", |                 "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", | ||||||
|                 "sha256:b4d59ab3608538e550a72cea13d3c209dd72b6e19e832688da7884081c01594e", |                 "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", | ||||||
|                 "sha256:b51d3f1cd87f488455f43046d72003689024b0fa9b2d53635db7523033b19996", |                 "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", | ||||||
|                 "sha256:c02105deda867d09cdd5088d08708f06d75759df6f83d8f7007b06f422908a30", |                 "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", | ||||||
|                 "sha256:c7b6032dc4490b0dcaf078f09f5b382dc35493cb7f473840368bf0de3196c2b6", |                 "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", | ||||||
|                 "sha256:c95b355dba2aaf5177dff943b25ded0529a7feb80021d5fdb114a99f0a1ef508", |                 "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", | ||||||
|                 "sha256:c980ae87863d76b1ea9a073d6d95554b4135032d34bc541be50c07d4a085821b", |                 "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", | ||||||
|                 "sha256:d12895cd083e35e9e032eb4b57645b91116f8979527381a8d864d1f6b8cb4a2e", |                 "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", | ||||||
|                 "sha256:d3cd9bad547a8e5fbe712a1dc1413aff1b917e8d39a2cd1389a6f933b7a21460", |                 "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", | ||||||
|                 "sha256:e8809b01f27f679e3023b9e2013051e0a3f17abff4228cb5197663afd8a0f2c7", |                 "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", | ||||||
|                 "sha256:f3c37b0dc1898e305aad4f7a1d75f6da83036588c28a9ce0afc681ff5245a601", |                 "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", | ||||||
|                 "sha256:f966765f54b536e791541458de84a737a6adba8467190f17a8fe7f85354ba908", |                 "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", | ||||||
|                 "sha256:fa939c2e2468142c9773443d4038e7c915b0cc1b670d3c9192bdc503f7ea73e9", |                 "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", | ||||||
|                 "sha256:fcc5c1f95102989d2e116ffc8467963554ce89f30a65a3ea86a4d06849c498d8" |                 "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |             "version": "==5.4.0" | ||||||
|             "version": "==5.3.0" |  | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "develop": { |     "develop": { | ||||||
| @ -1509,19 +1417,17 @@ | |||||||
|         }, |         }, | ||||||
|         "astroid": { |         "astroid": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:ad63b8552c70939568966811a088ef0bc880f99a24a00834abd0e3681b514f91", |                 "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", | ||||||
|                 "sha256:bea3f32799fbb8581f58431c12591bc20ce11cbc90ad82e2ea5717d94f2080d5" |                 "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "version": "==2.5.6" | ||||||
|             "version": "==2.5.3" |  | ||||||
|         }, |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", |                 "sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8", | ||||||
|                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" |                 "sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "version": "==21.1.0" | ||||||
|             "version": "==20.3.0" |  | ||||||
|         }, |         }, | ||||||
|         "bandit": { |         "bandit": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1546,12 +1452,25 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.0.1" |             "version": "==1.0.1" | ||||||
|         }, |         }, | ||||||
|  |         "certifi": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", | ||||||
|  |                 "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" | ||||||
|  |             ], | ||||||
|  |             "version": "==2020.12.5" | ||||||
|  |         }, | ||||||
|  |         "chardet": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", | ||||||
|  |                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" | ||||||
|  |             ], | ||||||
|  |             "version": "==4.0.0" | ||||||
|  |         }, | ||||||
|         "click": { |         "click": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", |                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" |                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", |  | ||||||
|             "version": "==7.1.2" |             "version": "==7.1.2" | ||||||
|         }, |         }, | ||||||
|         "colorama": { |         "colorama": { | ||||||
| @ -1625,16 +1544,21 @@ | |||||||
|                 "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", |                 "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", | ||||||
|                 "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" |                 "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.4'", |  | ||||||
|             "version": "==4.0.7" |             "version": "==4.0.7" | ||||||
|         }, |         }, | ||||||
|         "gitpython": { |         "gitpython": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b", |                 "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e", | ||||||
|                 "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61" |                 "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.4'", |             "version": "==3.1.15" | ||||||
|             "version": "==3.1.14" |         }, | ||||||
|  |         "idna": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||||
|  |                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||||
|  |             ], | ||||||
|  |             "version": "==2.10" | ||||||
|         }, |         }, | ||||||
|         "iniconfig": { |         "iniconfig": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1648,7 +1572,6 @@ | |||||||
|                 "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", |                 "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", | ||||||
|                 "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" |                 "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6' and python_version < '4.0'", |  | ||||||
|             "version": "==5.8.0" |             "version": "==5.8.0" | ||||||
|         }, |         }, | ||||||
|         "lazy-object-proxy": { |         "lazy-object-proxy": { | ||||||
| @ -1676,7 +1599,6 @@ | |||||||
|                 "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", |                 "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", | ||||||
|                 "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" |                 "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", |  | ||||||
|             "version": "==1.6.0" |             "version": "==1.6.0" | ||||||
|         }, |         }, | ||||||
|         "mccabe": { |         "mccabe": { | ||||||
| @ -1710,18 +1632,16 @@ | |||||||
|         }, |         }, | ||||||
|         "pbr": { |         "pbr": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", |                 "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", | ||||||
|                 "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" |                 "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6'", |             "version": "==5.6.0" | ||||||
|             "version": "==5.5.1" |  | ||||||
|         }, |         }, | ||||||
|         "pluggy": { |         "pluggy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", |                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", | ||||||
|                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" |                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==0.13.1" |             "version": "==0.13.1" | ||||||
|         }, |         }, | ||||||
|         "py": { |         "py": { | ||||||
| @ -1729,24 +1649,23 @@ | |||||||
|                 "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", |                 "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", | ||||||
|                 "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" |                 "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==1.10.0" |             "version": "==1.10.0" | ||||||
|         }, |         }, | ||||||
|         "pylint": { |         "pylint": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a", |                 "sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217", | ||||||
|                 "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee" |                 "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.7.4" |             "version": "==2.8.2" | ||||||
|         }, |         }, | ||||||
|         "pylint-django": { |         "pylint-django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:a5a4515209a6237d1d390a4a307d53f53baaf4f058ecf4bb556c775d208f6b0d", |                 "sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b", | ||||||
|                 "sha256:dc5ed27bb7662d73444ccd15a0b3964ed6ced6cc2712b85db616102062d2ec35" |                 "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.4.3" |             "version": "==2.4.4" | ||||||
|         }, |         }, | ||||||
|         "pylint-plugin-utils": { |         "pylint-plugin-utils": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1760,16 +1679,15 @@ | |||||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", |                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" |                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==2.4.7" |             "version": "==2.4.7" | ||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", |                 "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", | ||||||
|                 "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" |                 "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==6.2.3" |             "version": "==6.2.4" | ||||||
|         }, |         }, | ||||||
|         "pytest-django": { |         "pytest-django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1860,6 +1778,21 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==2021.4.4" |             "version": "==2021.4.4" | ||||||
|         }, |         }, | ||||||
|  |         "requests": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", | ||||||
|  |                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" | ||||||
|  |             ], | ||||||
|  |             "version": "==2.25.1" | ||||||
|  |         }, | ||||||
|  |         "requests-mock": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:33296f228d8c5df11a7988b741325422480baddfdf5dd9318fd0eb40c3ed8595", | ||||||
|  |                 "sha256:5c8ef0254c14a84744be146e9799dc13ebc4f6186058112d9aeed96b131b58e2" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==1.9.2" | ||||||
|  |         }, | ||||||
|         "selenium": { |         "selenium": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", |                 "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", | ||||||
| @ -1870,18 +1803,16 @@ | |||||||
|         }, |         }, | ||||||
|         "six": { |         "six": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", |                 "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", | ||||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" |                 "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "version": "==1.16.0" | ||||||
|             "version": "==1.15.0" |  | ||||||
|         }, |         }, | ||||||
|         "smmap": { |         "smmap": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", |                 "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", | ||||||
|                 "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" |                 "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.5'", |  | ||||||
|             "version": "==4.0.0" |             "version": "==4.0.0" | ||||||
|         }, |         }, | ||||||
|         "stevedore": { |         "stevedore": { | ||||||
| @ -1889,7 +1820,6 @@ | |||||||
|                 "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", |                 "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", | ||||||
|                 "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" |                 "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |  | ||||||
|             "version": "==3.3.0" |             "version": "==3.3.0" | ||||||
|         }, |         }, | ||||||
|         "toml": { |         "toml": { | ||||||
| @ -1897,7 +1827,6 @@ | |||||||
|                 "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", |                 "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", | ||||||
|                 "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" |                 "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", |  | ||||||
|             "version": "==0.10.2" |             "version": "==0.10.2" | ||||||
|         }, |         }, | ||||||
|         "typed-ast": { |         "typed-ast": { | ||||||
| @ -1937,11 +1866,11 @@ | |||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |         "typing-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", |                 "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", | ||||||
|                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", |                 "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", | ||||||
|                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" |                 "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.7.4.3" |             "version": "==3.10.0.0" | ||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
|             "extras": [ |             "extras": [ | ||||||
|  | |||||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							| @ -4,13 +4,14 @@ | |||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| [](https://discord.gg/KPnmtNWy) | [](https://discord.gg/jg33eMhnj6) | ||||||
| [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=1) | [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6) | ||||||
| [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=1) | [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6) | ||||||
| [](https://codecov.io/gh/BeryJu/authentik) | [](https://codecov.io/gh/goauthentik/authentik) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | [Transifex](https://www.transifex.com/beryjuorg/authentik/) | ||||||
|  |  | ||||||
| ## What is authentik? | ## What is authentik? | ||||||
|  |  | ||||||
| @ -31,7 +32,7 @@ Light | Dark | |||||||
|  |  | ||||||
| ## Development | ## Development | ||||||
|  |  | ||||||
| See [Development Documentation](https://goauthentik.io/docs/development/local-dev-environment) | See [Development Documentation](https://goauthentik.io/developer-docs/) | ||||||
|  |  | ||||||
| ## Security | ## Security | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,8 +4,8 @@ | |||||||
|  |  | ||||||
| | Version    | Supported          | | | Version    | Supported          | | ||||||
| | ---------- | ------------------ | | | ---------- | ------------------ | | ||||||
| | 2021.3.x   | :white_check_mark: | |  | ||||||
| | 2021.4.x   | :white_check_mark: | | | 2021.4.x   | :white_check_mark: | | ||||||
|  | | 2021.5.x   | :white_check_mark: | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,3 +1,3 @@ | |||||||
| """authentik""" | """authentik""" | ||||||
| __version__ = "2021.4.1-rc2" | __version__ = "2021.5.1-rc1" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from drf_yasg.utils import swagger_auto_schema | |||||||
| from packaging.version import parse | from packaging.version import parse | ||||||
| from rest_framework.fields import SerializerMethodField | from rest_framework.fields import SerializerMethodField | ||||||
| from rest_framework.mixins import ListModelMixin | from rest_framework.mixins import ListModelMixin | ||||||
| from rest_framework.permissions import IsAdminUser | from rest_framework.permissions import IsAuthenticated | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.viewsets import GenericViewSet | from rest_framework.viewsets import GenericViewSet | ||||||
| @ -50,7 +50,9 @@ class VersionSerializer(PassiveSerializer): | |||||||
| class VersionViewSet(ListModelMixin, GenericViewSet): | class VersionViewSet(ListModelMixin, GenericViewSet): | ||||||
|     """Get running and latest version.""" |     """Get running and latest version.""" | ||||||
|  |  | ||||||
|     permission_classes = [IsAdminUser] |     permission_classes = [IsAuthenticated] | ||||||
|  |     pagination_class = None | ||||||
|  |     filter_backends = [] | ||||||
|  |  | ||||||
|     def get_queryset(self):  # pragma: no cover |     def get_queryset(self):  # pragma: no cover | ||||||
|         return None |         return None | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from celery.schedules import crontab | |||||||
| CELERY_BEAT_SCHEDULE = { | CELERY_BEAT_SCHEDULE = { | ||||||
|     "admin_latest_version": { |     "admin_latest_version": { | ||||||
|         "task": "authentik.admin.tasks.update_latest_version", |         "task": "authentik.admin.tasks.update_latest_version", | ||||||
|         "schedule": crontab(minute=0),  # Run every hour |         "schedule": crontab(minute="*/60"),  # Run every hour | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -23,7 +23,9 @@ URL_FINDER = URLValidator.regex.pattern[1:] | |||||||
| def update_latest_version(self: MonitoredTask): | def update_latest_version(self: MonitoredTask): | ||||||
|     """Update latest version info""" |     """Update latest version info""" | ||||||
|     try: |     try: | ||||||
|         response = get("https://api.github.com/repos/beryju/authentik/releases/latest") |         response = get( | ||||||
|  |             "https://api.github.com/repos/goauthentik/authentik/releases/latest" | ||||||
|  |         ) | ||||||
|         response.raise_for_status() |         response.raise_for_status() | ||||||
|         data = response.json() |         data = response.json() | ||||||
|         tag_name = data.get("tag_name") |         tag_name = data.get("tag_name") | ||||||
|  | |||||||
| @ -4,58 +4,54 @@ from binascii import Error | |||||||
| from typing import Any, Optional, Union | from typing import Any, Optional, Union | ||||||
|  |  | ||||||
| from rest_framework.authentication import BaseAuthentication, get_authorization_header | from rest_framework.authentication import BaseAuthentication, get_authorization_header | ||||||
|  | from rest_framework.exceptions import AuthenticationFailed | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Token, TokenIntents, User | from authentik.core.models import Token, TokenIntents, User | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| X_AUTHENTIK_PREVENT_BASIC_HEADER = "HTTP_X_AUTHENTIK_PREVENT_BASIC" |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # pylint: disable=too-many-return-statements | ||||||
| def token_from_header(raw_header: bytes) -> Optional[Token]: | def token_from_header(raw_header: bytes) -> Optional[Token]: | ||||||
|     """raw_header in the Format of `Basic dGVzdDp0ZXN0`""" |     """raw_header in the Format of `Bearer dGVzdDp0ZXN0`""" | ||||||
|     auth_credentials = raw_header.decode() |     auth_credentials = raw_header.decode() | ||||||
|     # Accept headers with Type format and without |     if auth_credentials == "": | ||||||
|     if " " in auth_credentials: |  | ||||||
|         auth_type, auth_credentials = auth_credentials.split() |  | ||||||
|         if auth_type.lower() != "basic": |  | ||||||
|             LOGGER.debug( |  | ||||||
|                 "Unsupported authentication type, denying", type=auth_type.lower() |  | ||||||
|             ) |  | ||||||
|             return None |  | ||||||
|     try: |  | ||||||
|         auth_credentials = b64decode(auth_credentials.encode()).decode() |  | ||||||
|     except (UnicodeDecodeError, Error): |  | ||||||
|         return None |         return None | ||||||
|     # Accept credentials with username and without |     auth_type, auth_credentials = auth_credentials.split() | ||||||
|     if ":" in auth_credentials: |     if auth_type.lower() not in ["basic", "bearer"]: | ||||||
|         _, password = auth_credentials.split(":") |         LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower()) | ||||||
|     else: |         raise AuthenticationFailed("Unsupported authentication type") | ||||||
|         password = auth_credentials |     password = auth_credentials | ||||||
|  |     if auth_type.lower() == "basic": | ||||||
|  |         try: | ||||||
|  |             auth_credentials = b64decode(auth_credentials.encode()).decode() | ||||||
|  |         except (UnicodeDecodeError, Error): | ||||||
|  |             raise AuthenticationFailed("Malformed header") | ||||||
|  |         # Accept credentials with username and without | ||||||
|  |         if ":" in auth_credentials: | ||||||
|  |             _, password = auth_credentials.split(":") | ||||||
|  |         else: | ||||||
|  |             password = auth_credentials | ||||||
|     if password == "":  # nosec |     if password == "":  # nosec | ||||||
|         return None |         raise AuthenticationFailed("Malformed header") | ||||||
|     tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API) |     tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API) | ||||||
|     if not tokens.exists(): |     if not tokens.exists(): | ||||||
|         LOGGER.debug("Token not found") |         raise AuthenticationFailed("Token invalid/expired") | ||||||
|         return None |  | ||||||
|     return tokens.first() |     return tokens.first() | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikTokenAuthentication(BaseAuthentication): | class AuthentikTokenAuthentication(BaseAuthentication): | ||||||
|     """Token-based authentication using HTTP Basic authentication""" |     """Token-based authentication using HTTP Bearer authentication""" | ||||||
|  |  | ||||||
|     def authenticate(self, request: Request) -> Union[tuple[User, Any], None]: |     def authenticate(self, request: Request) -> Union[tuple[User, Any], None]: | ||||||
|         """Token-based authentication using HTTP Basic authentication""" |         """Token-based authentication using HTTP Bearer authentication""" | ||||||
|         auth = get_authorization_header(request) |         auth = get_authorization_header(request) | ||||||
|  |  | ||||||
|         token = token_from_header(auth) |         token = token_from_header(auth) | ||||||
|  |         # None is only returned when the header isn't set. | ||||||
|         if not token: |         if not token: | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|         return (token.user, None) |         return (token.user, None) | ||||||
|  |  | ||||||
|     def authenticate_header(self, request: Request) -> str: |  | ||||||
|         if X_AUTHENTIK_PREVENT_BASIC_HEADER in request._request.META: |  | ||||||
|             return "" |  | ||||||
|         return 'Basic realm="authentik"' |  | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| {% load static %} | {% load static %} | ||||||
|  |  | ||||||
| {% block title %} | {% block title %} | ||||||
| authentik API Browser | API Browser - {{ config.authentik.branding.title }} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block head %} | {% block head %} | ||||||
| @ -11,6 +11,29 @@ authentik API Browser | |||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block body %} | {% block body %} | ||||||
|  | <script> | ||||||
|  | function getCookie(name) { | ||||||
|  |     let cookieValue = ""; | ||||||
|  |     if (document.cookie && document.cookie !== "") { | ||||||
|  |         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-CSRFToken', getCookie("authentik_csrf")); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| <rapi-doc | <rapi-doc | ||||||
|     spec-url="{{ path }}" |     spec-url="{{ path }}" | ||||||
|     heading-text="authentik" |     heading-text="authentik" | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ from base64 import b64encode | |||||||
|  |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
|  | from rest_framework.exceptions import AuthenticationFailed | ||||||
|  |  | ||||||
| from authentik.api.auth import token_from_header | from authentik.api.auth import token_from_header | ||||||
| from authentik.core.models import Token, TokenIntents | from authentik.core.models import Token, TokenIntents | ||||||
| @ -11,7 +12,7 @@ from authentik.core.models import Token, TokenIntents | |||||||
| class TestAPIAuth(TestCase): | class TestAPIAuth(TestCase): | ||||||
|     """Test API Authentication""" |     """Test API Authentication""" | ||||||
|  |  | ||||||
|     def test_valid(self): |     def test_valid_basic(self): | ||||||
|         """Test valid token""" |         """Test valid token""" | ||||||
|         token = Token.objects.create( |         token = Token.objects.create( | ||||||
|             intent=TokenIntents.INTENT_API, user=get_anonymous_user() |             intent=TokenIntents.INTENT_API, user=get_anonymous_user() | ||||||
| @ -19,19 +20,30 @@ class TestAPIAuth(TestCase): | |||||||
|         auth = b64encode(f":{token.key}".encode()).decode() |         auth = b64encode(f":{token.key}".encode()).decode() | ||||||
|         self.assertEqual(token_from_header(f"Basic {auth}".encode()), token) |         self.assertEqual(token_from_header(f"Basic {auth}".encode()), token) | ||||||
|  |  | ||||||
|  |     def test_valid_bearer(self): | ||||||
|  |         """Test valid token""" | ||||||
|  |         token = Token.objects.create( | ||||||
|  |             intent=TokenIntents.INTENT_API, user=get_anonymous_user() | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(token_from_header(f"Bearer {token.key}".encode()), token) | ||||||
|  |  | ||||||
|     def test_invalid_type(self): |     def test_invalid_type(self): | ||||||
|         """Test invalid type""" |         """Test invalid type""" | ||||||
|         self.assertIsNone(token_from_header("foo bar".encode())) |         with self.assertRaises(AuthenticationFailed): | ||||||
|  |             token_from_header("foo bar".encode()) | ||||||
|  |  | ||||||
|     def test_invalid_decode(self): |     def test_invalid_decode(self): | ||||||
|         """Test invalid bas64""" |         """Test invalid bas64""" | ||||||
|         self.assertIsNone(token_from_header("Basic bar".encode())) |         with self.assertRaises(AuthenticationFailed): | ||||||
|  |             token_from_header("Basic bar".encode()) | ||||||
|  |  | ||||||
|     def test_invalid_empty_password(self): |     def test_invalid_empty_password(self): | ||||||
|         """Test invalid with empty password""" |         """Test invalid with empty password""" | ||||||
|         self.assertIsNone(token_from_header("Basic :".encode())) |         with self.assertRaises(AuthenticationFailed): | ||||||
|  |             token_from_header("Basic :".encode()) | ||||||
|  |  | ||||||
|     def test_invalid_no_token(self): |     def test_invalid_no_token(self): | ||||||
|         """Test invalid with no token""" |         """Test invalid with no token""" | ||||||
|         auth = b64encode(":abc".encode()).decode() |         with self.assertRaises(AuthenticationFailed): | ||||||
|         self.assertIsNone(token_from_header(f"Basic :{auth}".encode())) |             auth = b64encode(":abc".encode()).decode() | ||||||
|  |             self.assertIsNone(token_from_header(f"Basic :{auth}".encode())) | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								authentik/api/tests/test_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								authentik/api/tests/test_config.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | """Test config API""" | ||||||
|  | from json import loads | ||||||
|  |  | ||||||
|  | from django.urls import reverse | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestConfig(APITestCase): | ||||||
|  |     """Test config API""" | ||||||
|  |  | ||||||
|  |     def test_config(self): | ||||||
|  |         """Test YAML generation""" | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:configs-list"), | ||||||
|  |         ) | ||||||
|  |         self.assertTrue(loads(response.content.decode())) | ||||||
							
								
								
									
										33
									
								
								authentik/api/tests/test_decorators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								authentik/api/tests/test_decorators.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | """test decorators api""" | ||||||
|  | from django.urls import reverse | ||||||
|  | from guardian.shortcuts import assign_perm | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application, User | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestAPIDecorators(APITestCase): | ||||||
|  |     """test decorators api""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         super().setUp() | ||||||
|  |         self.user = User.objects.create(username="test-user") | ||||||
|  |  | ||||||
|  |     def test_obj_perm_denied(self): | ||||||
|  |         """Test object perm denied""" | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         app = Application.objects.create(name="denied", slug="denied") | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|  |     def test_other_perm_denied(self): | ||||||
|  |         """Test other perm denied""" | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         app = Application.objects.create(name="denied", slug="denied") | ||||||
|  |         assign_perm("authentik_core.view_application", self.user, app) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:application-metrics", kwargs={"slug": app.slug}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 403) | ||||||
| @ -47,6 +47,7 @@ from authentik.policies.reputation.api import ( | |||||||
|     ReputationPolicyViewSet, |     ReputationPolicyViewSet, | ||||||
|     UserReputationViewSet, |     UserReputationViewSet, | ||||||
| ) | ) | ||||||
|  | from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet | ||||||
| from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet | from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet | ||||||
| from authentik.providers.oauth2.api.scope import ScopeMappingViewSet | from authentik.providers.oauth2.api.scope import ScopeMappingViewSet | ||||||
| from authentik.providers.oauth2.api.tokens import ( | from authentik.providers.oauth2.api.tokens import ( | ||||||
| @ -63,6 +64,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet | |||||||
| from authentik.sources.oauth.api.source_connection import ( | from authentik.sources.oauth.api.source_connection import ( | ||||||
|     UserOAuthSourceConnectionViewSet, |     UserOAuthSourceConnectionViewSet, | ||||||
| ) | ) | ||||||
|  | from authentik.sources.plex.api import PlexSourceViewSet | ||||||
| from authentik.sources.saml.api import SAMLSourceViewSet | from authentik.sources.saml.api import SAMLSourceViewSet | ||||||
| from authentik.stages.authenticator_static.api import ( | from authentik.stages.authenticator_static.api import ( | ||||||
|     AuthenticatorStaticStageViewSet, |     AuthenticatorStaticStageViewSet, | ||||||
| @ -113,12 +115,14 @@ router.register("core/user_consent", UserConsentViewSet) | |||||||
| router.register("core/tokens", TokenViewSet) | router.register("core/tokens", TokenViewSet) | ||||||
|  |  | ||||||
| router.register("outposts/outposts", OutpostViewSet) | router.register("outposts/outposts", OutpostViewSet) | ||||||
|  | router.register("outposts/instances", OutpostViewSet) | ||||||
| router.register("outposts/service_connections/all", ServiceConnectionViewSet) | router.register("outposts/service_connections/all", ServiceConnectionViewSet) | ||||||
| router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) | router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) | ||||||
| router.register( | router.register( | ||||||
|     "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet |     "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet | ||||||
| ) | ) | ||||||
| router.register("outposts/proxy", ProxyOutpostConfigViewSet) | router.register("outposts/proxy", ProxyOutpostConfigViewSet) | ||||||
|  | router.register("outposts/ldap", LDAPOutpostConfigViewSet) | ||||||
|  |  | ||||||
| router.register("flows/instances", FlowViewSet) | router.register("flows/instances", FlowViewSet) | ||||||
| router.register("flows/bindings", FlowStageBindingViewSet) | router.register("flows/bindings", FlowStageBindingViewSet) | ||||||
| @ -135,6 +139,7 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS | |||||||
| router.register("sources/ldap", LDAPSourceViewSet) | router.register("sources/ldap", LDAPSourceViewSet) | ||||||
| router.register("sources/saml", SAMLSourceViewSet) | router.register("sources/saml", SAMLSourceViewSet) | ||||||
| router.register("sources/oauth", OAuthSourceViewSet) | router.register("sources/oauth", OAuthSourceViewSet) | ||||||
|  | router.register("sources/plex", PlexSourceViewSet) | ||||||
|  |  | ||||||
| router.register("policies/all", PolicyViewSet) | router.register("policies/all", PolicyViewSet) | ||||||
| router.register("policies/bindings", PolicyBindingViewSet) | router.register("policies/bindings", PolicyBindingViewSet) | ||||||
| @ -148,6 +153,7 @@ router.register("policies/reputation/ips", IPReputationViewSet) | |||||||
| router.register("policies/reputation", ReputationPolicyViewSet) | router.register("policies/reputation", ReputationPolicyViewSet) | ||||||
|  |  | ||||||
| router.register("providers/all", ProviderViewSet) | router.register("providers/all", ProviderViewSet) | ||||||
|  | router.register("providers/ldap", LDAPProviderViewSet) | ||||||
| router.register("providers/proxy", ProxyProviderViewSet) | 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) | ||||||
| @ -195,7 +201,8 @@ info = openapi.Info( | |||||||
|     default_version="v2beta", |     default_version="v2beta", | ||||||
|     contact=openapi.Contact(email="hello@beryju.org"), |     contact=openapi.Contact(email="hello@beryju.org"), | ||||||
|     license=openapi.License( |     license=openapi.License( | ||||||
|         name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE" |         name="GNU GPLv3", | ||||||
|  |         url="https://github.com/goauthentik/authentik/blob/master/LICENSE", | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| SchemaView = get_schema_view(info, public=True, permission_classes=(AllowAny,)) | SchemaView = get_schema_view(info, public=True, permission_classes=(AllowAny,)) | ||||||
|  | |||||||
| @ -91,6 +91,32 @@ class ApplicationViewSet(ModelViewSet): | |||||||
|                 applications.append(application) |                 applications.append(application) | ||||||
|         return applications |         return applications | ||||||
|  |  | ||||||
|  |     @swagger_auto_schema( | ||||||
|  |         responses={ | ||||||
|  |             204: "Access granted", | ||||||
|  |             403: "Access denied", | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     @action(detail=True, methods=["GET"]) | ||||||
|  |     # pylint: disable=unused-argument | ||||||
|  |     def check_access(self, request: Request, slug: str) -> Response: | ||||||
|  |         """Check access to a single application by slug""" | ||||||
|  |         application = self.get_object() | ||||||
|  |         engine = PolicyEngine(application, self.request.user, self.request) | ||||||
|  |         engine.build() | ||||||
|  |         if engine.passing: | ||||||
|  |             return Response(status=204) | ||||||
|  |         return Response(status=403) | ||||||
|  |  | ||||||
|  |     @swagger_auto_schema( | ||||||
|  |         manual_parameters=[ | ||||||
|  |             openapi.Parameter( | ||||||
|  |                 name="superuser_full_list", | ||||||
|  |                 in_=openapi.IN_QUERY, | ||||||
|  |                 type=openapi.TYPE_BOOLEAN, | ||||||
|  |             ) | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|     def list(self, request: Request) -> Response: |     def list(self, request: Request) -> Response: | ||||||
|         """Custom list method that checks Policy based access instead of guardian""" |         """Custom list method that checks Policy based access instead of guardian""" | ||||||
|         queryset = self._filter_queryset_for_list(self.get_queryset()) |         queryset = self._filter_queryset_for_list(self.get_queryset()) | ||||||
| @ -98,6 +124,13 @@ class ApplicationViewSet(ModelViewSet): | |||||||
|  |  | ||||||
|         should_cache = request.GET.get("search", "") == "" |         should_cache = request.GET.get("search", "") == "" | ||||||
|  |  | ||||||
|  |         superuser_full_list = ( | ||||||
|  |             str(request.GET.get("superuser_full_list", "false")).lower() == "true" | ||||||
|  |         ) | ||||||
|  |         if superuser_full_list and request.user.is_superuser: | ||||||
|  |             serializer = self.get_serializer(queryset, many=True) | ||||||
|  |             return self.get_paginated_response(serializer.data) | ||||||
|  |  | ||||||
|         allowed_applications = [] |         allowed_applications = [] | ||||||
|         if not should_cache: |         if not should_cache: | ||||||
|             allowed_applications = self._get_allowed_applications(queryset) |             allowed_applications = self._get_allowed_applications(queryset) | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| """Groups API Viewset""" | """Groups API Viewset""" | ||||||
|  | from django.db.models.query import QuerySet | ||||||
| from rest_framework.fields import JSONField | from rest_framework.fields import JSONField | ||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  | from rest_framework_guardian.filters import ObjectPermissionsFilter | ||||||
|  |  | ||||||
| from authentik.core.api.utils import is_dict | from authentik.core.api.utils import is_dict | ||||||
| from authentik.core.models import Group | from authentik.core.models import Group | ||||||
| @ -26,3 +28,16 @@ class GroupViewSet(ModelViewSet): | |||||||
|     search_fields = ["name", "is_superuser"] |     search_fields = ["name", "is_superuser"] | ||||||
|     filterset_fields = ["name", "is_superuser"] |     filterset_fields = ["name", "is_superuser"] | ||||||
|     ordering = ["name"] |     ordering = ["name"] | ||||||
|  |  | ||||||
|  |     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: | ||||||
|  |         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||||
|  |         for backend in list(self.filter_backends): | ||||||
|  |             if backend == ObjectPermissionsFilter: | ||||||
|  |                 continue | ||||||
|  |             queryset = backend().filter_queryset(self.request, queryset, self) | ||||||
|  |         return queryset | ||||||
|  |  | ||||||
|  |     def filter_queryset(self, queryset): | ||||||
|  |         if self.request.user.has_perm("authentik_core.view_group"): | ||||||
|  |             return self._filter_queryset_for_list(queryset) | ||||||
|  |         return super().filter_queryset(queryset) | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """PropertyMapping API Views""" | """PropertyMapping API Views""" | ||||||
| from json import dumps | from json import dumps | ||||||
|  |  | ||||||
|  | from drf_yasg import openapi | ||||||
| from drf_yasg.utils import swagger_auto_schema | from drf_yasg.utils import swagger_auto_schema | ||||||
| from guardian.shortcuts import get_objects_for_user | from guardian.shortcuts import get_objects_for_user | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
| @ -91,7 +92,9 @@ class PropertyMappingViewSet( | |||||||
|                 { |                 { | ||||||
|                     "name": subclass._meta.verbose_name, |                     "name": subclass._meta.verbose_name, | ||||||
|                     "description": subclass.__doc__, |                     "description": subclass.__doc__, | ||||||
|                     "component": subclass.component, |                     # pyright: reportGeneralTypeIssues=false | ||||||
|  |                     "component": subclass().component, | ||||||
|  |                     "model_name": subclass._meta.model_name, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
| @ -100,6 +103,13 @@ class PropertyMappingViewSet( | |||||||
|     @swagger_auto_schema( |     @swagger_auto_schema( | ||||||
|         request_body=PolicyTestSerializer(), |         request_body=PolicyTestSerializer(), | ||||||
|         responses={200: PropertyMappingTestResultSerializer, 400: "Invalid parameters"}, |         responses={200: PropertyMappingTestResultSerializer, 400: "Invalid parameters"}, | ||||||
|  |         manual_parameters=[ | ||||||
|  |             openapi.Parameter( | ||||||
|  |                 name="format_result", | ||||||
|  |                 in_=openapi.IN_QUERY, | ||||||
|  |                 type=openapi.TYPE_BOOLEAN, | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|     ) |     ) | ||||||
|     @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"]) |     @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"]) | ||||||
|     # pylint: disable=unused-argument, invalid-name |     # pylint: disable=unused-argument, invalid-name | ||||||
| @ -110,6 +120,8 @@ class PropertyMappingViewSet( | |||||||
|         if not test_params.is_valid(): |         if not test_params.is_valid(): | ||||||
|             return Response(test_params.errors, status=400) |             return Response(test_params.errors, status=400) | ||||||
|  |  | ||||||
|  |         format_result = str(request.GET.get("format_result", "false")).lower() == "true" | ||||||
|  |  | ||||||
|         # User permission check, only allow mapping testing for users that are readable |         # User permission check, only allow mapping testing for users that are readable | ||||||
|         users = get_objects_for_user(request.user, "authentik_core.view_user").filter( |         users = get_objects_for_user(request.user, "authentik_core.view_user").filter( | ||||||
|             pk=test_params.validated_data["user"].pk |             pk=test_params.validated_data["user"].pk | ||||||
| @ -124,7 +136,9 @@ class PropertyMappingViewSet( | |||||||
|                 self.request, |                 self.request, | ||||||
|                 **test_params.validated_data.get("context", {}), |                 **test_params.validated_data.get("context", {}), | ||||||
|             ) |             ) | ||||||
|             response_data["result"] = dumps(result) |             response_data["result"] = dumps( | ||||||
|  |                 result, indent=(4 if format_result else None) | ||||||
|  |             ) | ||||||
|         except Exception as exc:  # pylint: disable=broad-except |         except Exception as exc:  # pylint: disable=broad-except | ||||||
|             response_data["result"] = str(exc) |             response_data["result"] = str(exc) | ||||||
|             response_data["successful"] = False |             response_data["successful"] = False | ||||||
|  | |||||||
| @ -78,6 +78,7 @@ class ProviderViewSet( | |||||||
|                     "name": subclass._meta.verbose_name, |                     "name": subclass._meta.verbose_name, | ||||||
|                     "description": subclass.__doc__, |                     "description": subclass.__doc__, | ||||||
|                     "component": subclass().component, |                     "component": subclass().component, | ||||||
|  |                     "model_name": subclass._meta.model_name, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         data.append( |         data.append( | ||||||
| @ -85,6 +86,7 @@ class ProviderViewSet( | |||||||
|                 "name": _("SAML Provider from Metadata"), |                 "name": _("SAML Provider from Metadata"), | ||||||
|                 "description": _("Create a SAML Provider by importing its Metadata."), |                 "description": _("Create a SAML Provider by importing its Metadata."), | ||||||
|                 "component": "ak-provider-saml-import-form", |                 "component": "ak-provider-saml-import-form", | ||||||
|  |                 "model_name": "", | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
|  | |||||||
| @ -45,6 +45,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|             "policy_engine_mode", |             "policy_engine_mode", | ||||||
|  |             "user_matching_mode", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -81,6 +82,7 @@ class SourceViewSet( | |||||||
|                     "name": subclass._meta.verbose_name, |                     "name": subclass._meta.verbose_name, | ||||||
|                     "description": subclass.__doc__, |                     "description": subclass.__doc__, | ||||||
|                     "component": component, |                     "component": component, | ||||||
|  |                     "model_name": subclass._meta.model_name, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from authentik.api.decorators import permission_required | from authentik.api.decorators import permission_required | ||||||
| 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 | ||||||
| from authentik.core.models import Token | from authentik.core.models import Token, TokenIntents | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.managed.api import ManagedSerializer | from authentik.managed.api import ManagedSerializer | ||||||
|  |  | ||||||
| @ -64,7 +64,7 @@ class TokenViewSet(ModelViewSet): | |||||||
|     ordering = ["expires"] |     ordering = ["expires"] | ||||||
|  |  | ||||||
|     def perform_create(self, serializer: TokenSerializer): |     def perform_create(self, serializer: TokenSerializer): | ||||||
|         serializer.save(user=self.request.user) |         serializer.save(user=self.request.user, intent=TokenIntents.INTENT_API) | ||||||
|  |  | ||||||
|     @permission_required("authentik_core.view_token_key") |     @permission_required("authentik_core.view_token_key") | ||||||
|     @swagger_auto_schema( |     @swagger_auto_schema( | ||||||
|  | |||||||
| @ -1,17 +1,30 @@ | |||||||
| """User API Views""" | """User API Views""" | ||||||
|  | from json import loads | ||||||
|  |  | ||||||
|  | from django.db.models.query import QuerySet | ||||||
|  | from django.http.response import Http404 | ||||||
| from django.urls import reverse_lazy | from django.urls import reverse_lazy | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
|  | from django_filters.filters import BooleanFilter, CharFilter | ||||||
|  | from django_filters.filterset import FilterSet | ||||||
| from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method | from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method | ||||||
| from guardian.utils import get_anonymous_user | from guardian.utils import get_anonymous_user | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import CharField, JSONField, SerializerMethodField | from rest_framework.fields import CharField, JSONField, SerializerMethodField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import BooleanField, ModelSerializer | from rest_framework.serializers import ( | ||||||
|  |     BooleanField, | ||||||
|  |     ListSerializer, | ||||||
|  |     ModelSerializer, | ||||||
|  |     ValidationError, | ||||||
|  | ) | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  | from rest_framework_guardian.filters import ObjectPermissionsFilter | ||||||
|  |  | ||||||
| from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h | from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h | ||||||
| from authentik.api.decorators import permission_required | from authentik.api.decorators import permission_required | ||||||
|  | from authentik.core.api.groups import GroupSerializer | ||||||
| from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict | from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict | ||||||
| from authentik.core.middleware import ( | from authentik.core.middleware import ( | ||||||
|     SESSION_IMPERSONATE_ORIGINAL_USER, |     SESSION_IMPERSONATE_ORIGINAL_USER, | ||||||
| @ -19,6 +32,7 @@ from authentik.core.middleware import ( | |||||||
| ) | ) | ||||||
| from authentik.core.models import Token, TokenIntents, User | from authentik.core.models import Token, TokenIntents, User | ||||||
| from authentik.events.models import EventAction | from authentik.events.models import EventAction | ||||||
|  | from authentik.flows.models import Flow, FlowDesignation | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserSerializer(ModelSerializer): | class UserSerializer(ModelSerializer): | ||||||
| @ -27,6 +41,8 @@ class UserSerializer(ModelSerializer): | |||||||
|     is_superuser = BooleanField(read_only=True) |     is_superuser = BooleanField(read_only=True) | ||||||
|     avatar = CharField(read_only=True) |     avatar = CharField(read_only=True) | ||||||
|     attributes = JSONField(validators=[is_dict], required=False) |     attributes = JSONField(validators=[is_dict], required=False) | ||||||
|  |     groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups") | ||||||
|  |     uid = CharField(read_only=True) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
| @ -38,9 +54,11 @@ class UserSerializer(ModelSerializer): | |||||||
|             "is_active", |             "is_active", | ||||||
|             "last_login", |             "last_login", | ||||||
|             "is_superuser", |             "is_superuser", | ||||||
|  |             "groups", | ||||||
|             "email", |             "email", | ||||||
|             "avatar", |             "avatar", | ||||||
|             "attributes", |             "attributes", | ||||||
|  |             "uid", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -82,13 +100,44 @@ class UserMetricsSerializer(PassiveSerializer): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UsersFilter(FilterSet): | ||||||
|  |     """Filter for users""" | ||||||
|  |  | ||||||
|  |     attributes = CharFilter( | ||||||
|  |         field_name="attributes", | ||||||
|  |         lookup_expr="", | ||||||
|  |         label="Attributes", | ||||||
|  |         method="filter_attributes", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser") | ||||||
|  |  | ||||||
|  |     # pylint: disable=unused-argument | ||||||
|  |     def filter_attributes(self, queryset, name, value): | ||||||
|  |         """Filter attributes by query args""" | ||||||
|  |         try: | ||||||
|  |             value = loads(value) | ||||||
|  |         except ValueError: | ||||||
|  |             raise ValidationError(detail="filter: failed to parse JSON") | ||||||
|  |         if not isinstance(value, dict): | ||||||
|  |             raise ValidationError(detail="filter: value must be key:value mapping") | ||||||
|  |         qs = {} | ||||||
|  |         for key, _value in value.items(): | ||||||
|  |             qs[f"attributes__{key}"] = _value | ||||||
|  |         return queryset.filter(**qs) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = User | ||||||
|  |         fields = ["username", "name", "is_active", "is_superuser", "attributes"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserViewSet(ModelViewSet): | class UserViewSet(ModelViewSet): | ||||||
|     """User Viewset""" |     """User Viewset""" | ||||||
|  |  | ||||||
|     queryset = User.objects.none() |     queryset = User.objects.none() | ||||||
|     serializer_class = UserSerializer |     serializer_class = UserSerializer | ||||||
|     search_fields = ["username", "name", "is_active"] |     search_fields = ["username", "name", "is_active"] | ||||||
|     filterset_fields = ["username", "name", "is_active"] |     filterset_class = UsersFilter | ||||||
|  |  | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         return User.objects.all().exclude(pk=get_anonymous_user().pk) |         return User.objects.all().exclude(pk=get_anonymous_user().pk) | ||||||
| @ -121,12 +170,16 @@ class UserViewSet(ModelViewSet): | |||||||
|  |  | ||||||
|     @permission_required("authentik_core.reset_user_password") |     @permission_required("authentik_core.reset_user_password") | ||||||
|     @swagger_auto_schema( |     @swagger_auto_schema( | ||||||
|         responses={"200": LinkSerializer(many=False)}, |         responses={"200": LinkSerializer(many=False), "404": "No recovery flow found."}, | ||||||
|     ) |     ) | ||||||
|     @action(detail=True, pagination_class=None, filter_backends=[]) |     @action(detail=True, pagination_class=None, filter_backends=[]) | ||||||
|     # pylint: disable=invalid-name, unused-argument |     # pylint: disable=invalid-name, unused-argument | ||||||
|     def recovery(self, request: Request, pk: int) -> Response: |     def recovery(self, request: Request, pk: int) -> Response: | ||||||
|         """Create a temporary link that a user can use to recover their accounts""" |         """Create a temporary link that a user can use to recover their accounts""" | ||||||
|  |         # Check that there is a recovery flow, if not return an error | ||||||
|  |         flow = Flow.with_policy(request, designation=FlowDesignation.RECOVERY) | ||||||
|  |         if not flow: | ||||||
|  |             raise Http404 | ||||||
|         user: User = self.get_object() |         user: User = self.get_object() | ||||||
|         token, __ = Token.objects.get_or_create( |         token, __ = Token.objects.get_or_create( | ||||||
|             identifier=f"{user.uid}-password-reset", |             identifier=f"{user.uid}-password-reset", | ||||||
| @ -138,3 +191,16 @@ class UserViewSet(ModelViewSet): | |||||||
|             reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" |             reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" | ||||||
|         ) |         ) | ||||||
|         return Response({"link": link}) |         return Response({"link": link}) | ||||||
|  |  | ||||||
|  |     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: | ||||||
|  |         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||||
|  |         for backend in list(self.filter_backends): | ||||||
|  |             if backend == ObjectPermissionsFilter: | ||||||
|  |                 continue | ||||||
|  |             queryset = backend().filter_queryset(self.request, queryset, self) | ||||||
|  |         return queryset | ||||||
|  |  | ||||||
|  |     def filter_queryset(self, queryset): | ||||||
|  |         if self.request.user.has_perm("authentik_core.view_group"): | ||||||
|  |             return self._filter_queryset_for_list(queryset) | ||||||
|  |         return super().filter_queryset(queryset) | ||||||
|  | |||||||
| @ -20,10 +20,12 @@ def is_dict(value: Any): | |||||||
| class PassiveSerializer(Serializer): | class PassiveSerializer(Serializer): | ||||||
|     """Base serializer class which doesn't implement create/update methods""" |     """Base serializer class which doesn't implement create/update methods""" | ||||||
|  |  | ||||||
|     def create(self, validated_data: dict) -> Model: |     def create(self, validated_data: dict) -> Model:  # pragma: no cover | ||||||
|         return Model() |         return Model() | ||||||
|  |  | ||||||
|     def update(self, instance: Model, validated_data: dict) -> Model: |     def update( | ||||||
|  |         self, instance: Model, validated_data: dict | ||||||
|  |     ) -> Model:  # pragma: no cover | ||||||
|         return Model() |         return Model() | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -48,6 +50,7 @@ class TypeCreateSerializer(PassiveSerializer): | |||||||
|     name = CharField(required=True) |     name = CharField(required=True) | ||||||
|     description = CharField(required=True) |     description = CharField(required=True) | ||||||
|     component = CharField(required=True) |     component = CharField(required=True) | ||||||
|  |     model_name = CharField(required=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| class CacheSerializer(PassiveSerializer): | class CacheSerializer(PassiveSerializer): | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """Channels base classes""" | """Channels base classes""" | ||||||
| from channels.exceptions import DenyConnection | from channels.exceptions import DenyConnection | ||||||
| from channels.generic.websocket import JsonWebsocketConsumer | from channels.generic.websocket import JsonWebsocketConsumer | ||||||
|  | from rest_framework.exceptions import AuthenticationFailed | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.auth import token_from_header | from authentik.api.auth import token_from_header | ||||||
| @ -22,9 +23,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer): | |||||||
|  |  | ||||||
|         raw_header = headers[b"authorization"] |         raw_header = headers[b"authorization"] | ||||||
|  |  | ||||||
|         token = token_from_header(raw_header) |         try: | ||||||
|         if not token: |             token = token_from_header(raw_header) | ||||||
|             LOGGER.warning("Failed to authenticate") |             # token is only None when no header was given, in which case we deny too | ||||||
|  |             if not token: | ||||||
|  |                 raise DenyConnection() | ||||||
|  |         except AuthenticationFailed as exc: | ||||||
|  |             LOGGER.warning("Failed to authenticate", exc=exc) | ||||||
|             raise DenyConnection() |             raise DenyConnection() | ||||||
|  |  | ||||||
|         self.user = token.user |         self.user = token.user | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								authentik/core/migrations/0020_source_user_matching_mode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								authentik/core/migrations/0020_source_user_matching_mode.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-05-03 17:06 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0019_source_managed"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="source", | ||||||
|  |             name="user_matching_mode", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("identifier", "Use the source-specific identifier"), | ||||||
|  |                     ( | ||||||
|  |                         "email_link", | ||||||
|  |                         "Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "email_deny", | ||||||
|  |                         "Use the user's email address, but deny enrollment when the email address already exists.", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "username_link", | ||||||
|  |                         "Link to a user with identical username address. Can have security implications when a username is used with another source.", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "username_deny", | ||||||
|  |                         "Use the user's username, but deny enrollment when the username already exists.", | ||||||
|  |                     ), | ||||||
|  |                 ], | ||||||
|  |                 default="identifier", | ||||||
|  |                 help_text="How the source determines if an existing user should be authenticated or a new user enrolled.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -34,6 +34,7 @@ from authentik.policies.models import PolicyBindingModel | |||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | ||||||
| USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" | USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" | ||||||
|  | USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources" | ||||||
|  |  | ||||||
| GRAVATAR_URL = "https://secure.gravatar.com" | GRAVATAR_URL = "https://secure.gravatar.com" | ||||||
| DEFAULT_AVATAR = static("dist/assets/images/user_default.png") | DEFAULT_AVATAR = static("dist/assets/images/user_default.png") | ||||||
| @ -240,6 +241,30 @@ class Application(PolicyBindingModel): | |||||||
|         verbose_name_plural = _("Applications") |         verbose_name_plural = _("Applications") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SourceUserMatchingModes(models.TextChoices): | ||||||
|  |     """Different modes a source can handle new/returning users""" | ||||||
|  |  | ||||||
|  |     IDENTIFIER = "identifier", _("Use the source-specific identifier") | ||||||
|  |     EMAIL_LINK = "email_link", _( | ||||||
|  |         ( | ||||||
|  |             "Link to a user with identical email address. Can have security implications " | ||||||
|  |             "when a source doesn't validate email addresses." | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |     EMAIL_DENY = "email_deny", _( | ||||||
|  |         "Use the user's email address, but deny enrollment when the email address already exists." | ||||||
|  |     ) | ||||||
|  |     USERNAME_LINK = "username_link", _( | ||||||
|  |         ( | ||||||
|  |             "Link to a user with identical username address. Can have security implications " | ||||||
|  |             "when a username is used with another source." | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |     USERNAME_DENY = "username_deny", _( | ||||||
|  |         "Use the user's username, but deny enrollment when the username already exists." | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Source(ManagedModel, SerializerModel, PolicyBindingModel): | class Source(ManagedModel, SerializerModel, PolicyBindingModel): | ||||||
|     """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" |     """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" | ||||||
|  |  | ||||||
| @ -272,6 +297,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | |||||||
|         related_name="source_enrollment", |         related_name="source_enrollment", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     user_matching_mode = models.TextField( | ||||||
|  |         choices=SourceUserMatchingModes.choices, | ||||||
|  |         default=SourceUserMatchingModes.IDENTIFIER, | ||||||
|  |         help_text=_( | ||||||
|  |             ( | ||||||
|  |                 "How the source determines if an existing user should be authenticated or " | ||||||
|  |                 "a new user enrolled." | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     objects = InheritanceManager() |     objects = InheritanceManager() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @ -301,6 +337,8 @@ class UserSourceConnection(CreatedUpdatedModel): | |||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|     source = models.ForeignKey(Source, on_delete=models.CASCADE) |     source = models.ForeignKey(Source, on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|  |     objects = InheritanceManager() | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         unique_together = (("user", "source"),) |         unique_together = (("user", "source"),) | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								authentik/core/sources/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/core/sources/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										279
									
								
								authentik/core/sources/flow_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								authentik/core/sources/flow_manager.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,279 @@ | |||||||
|  | """Source decision helper""" | ||||||
|  | from enum import Enum | ||||||
|  | from typing import Any, Optional, Type | ||||||
|  |  | ||||||
|  | from django.contrib import messages | ||||||
|  | from django.db.models.query_utils import Q | ||||||
|  | from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest | ||||||
|  | from django.shortcuts import redirect | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.translation import gettext as _ | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.core.models import ( | ||||||
|  |     Source, | ||||||
|  |     SourceUserMatchingModes, | ||||||
|  |     User, | ||||||
|  |     UserSourceConnection, | ||||||
|  | ) | ||||||
|  | from authentik.core.sources.stage import ( | ||||||
|  |     PLAN_CONTEXT_SOURCES_CONNECTION, | ||||||
|  |     PostUserEnrollmentStage, | ||||||
|  | ) | ||||||
|  | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.flows.models import Flow, Stage, in_memory_stage | ||||||
|  | from authentik.flows.planner import ( | ||||||
|  |     PLAN_CONTEXT_PENDING_USER, | ||||||
|  |     PLAN_CONTEXT_REDIRECT, | ||||||
|  |     PLAN_CONTEXT_SOURCE, | ||||||
|  |     PLAN_CONTEXT_SSO, | ||||||
|  |     FlowPlanner, | ||||||
|  | ) | ||||||
|  | from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
|  | from authentik.lib.utils.urls import redirect_with_qs | ||||||
|  | from authentik.policies.utils import delete_none_keys | ||||||
|  | from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | ||||||
|  | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Action(Enum): | ||||||
|  |     """Actions that can be decided based on the request | ||||||
|  |     and source settings""" | ||||||
|  |  | ||||||
|  |     LINK = "link" | ||||||
|  |     AUTH = "auth" | ||||||
|  |     ENROLL = "enroll" | ||||||
|  |     DENY = "deny" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SourceFlowManager: | ||||||
|  |     """Help sources decide what they should do after authorization. Based on source settings and | ||||||
|  |     previous connections, authenticate the user, enroll a new user, link to an existing user | ||||||
|  |     or deny the request.""" | ||||||
|  |  | ||||||
|  |     source: Source | ||||||
|  |     request: HttpRequest | ||||||
|  |  | ||||||
|  |     identifier: str | ||||||
|  |  | ||||||
|  |     connection_type: Type[UserSourceConnection] = UserSourceConnection | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         source: Source, | ||||||
|  |         request: HttpRequest, | ||||||
|  |         identifier: str, | ||||||
|  |         enroll_info: dict[str, Any], | ||||||
|  |     ) -> None: | ||||||
|  |         self.source = source | ||||||
|  |         self.request = request | ||||||
|  |         self.identifier = identifier | ||||||
|  |         self.enroll_info = enroll_info | ||||||
|  |         self._logger = get_logger().bind(source=source, identifier=identifier) | ||||||
|  |  | ||||||
|  |     # pylint: disable=too-many-return-statements | ||||||
|  |     def get_action(self, **kwargs) -> tuple[Action, Optional[UserSourceConnection]]: | ||||||
|  |         """decide which action should be taken""" | ||||||
|  |         new_connection = self.connection_type( | ||||||
|  |             source=self.source, identifier=self.identifier | ||||||
|  |         ) | ||||||
|  |         # When request is authenticated, always link | ||||||
|  |         if self.request.user.is_authenticated: | ||||||
|  |             new_connection.user = self.request.user | ||||||
|  |             new_connection = self.update_connection(new_connection, **kwargs) | ||||||
|  |             new_connection.save() | ||||||
|  |             return Action.LINK, new_connection | ||||||
|  |  | ||||||
|  |         existing_connections = self.connection_type.objects.filter( | ||||||
|  |             source=self.source, identifier=self.identifier | ||||||
|  |         ) | ||||||
|  |         if existing_connections.exists(): | ||||||
|  |             connection = existing_connections.first() | ||||||
|  |             return Action.AUTH, self.update_connection(connection, **kwargs) | ||||||
|  |         # No connection exists, but we match on identifier, so enroll | ||||||
|  |         if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER: | ||||||
|  |             # We don't save the connection here cause it doesn't have a user assigned yet | ||||||
|  |             return Action.ENROLL, self.update_connection(new_connection, **kwargs) | ||||||
|  |  | ||||||
|  |         # Check for existing users with matching attributes | ||||||
|  |         query = Q() | ||||||
|  |         # Either query existing user based on email or username | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.EMAIL_LINK, | ||||||
|  |             SourceUserMatchingModes.EMAIL_DENY, | ||||||
|  |         ]: | ||||||
|  |             if not self.enroll_info.get("email", None): | ||||||
|  |                 self._logger.warning("Refusing to use none email", source=self.source) | ||||||
|  |                 return Action.DENY, None | ||||||
|  |             query = Q(email__exact=self.enroll_info.get("email", None)) | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.USERNAME_LINK, | ||||||
|  |             SourceUserMatchingModes.USERNAME_DENY, | ||||||
|  |         ]: | ||||||
|  |             if not self.enroll_info.get("username", None): | ||||||
|  |                 self._logger.warning( | ||||||
|  |                     "Refusing to use none username", source=self.source | ||||||
|  |                 ) | ||||||
|  |                 return Action.DENY, None | ||||||
|  |             query = Q(username__exact=self.enroll_info.get("username", None)) | ||||||
|  |         matching_users = User.objects.filter(query) | ||||||
|  |         # No matching users, always enroll | ||||||
|  |         if not matching_users.exists(): | ||||||
|  |             return Action.ENROLL, self.update_connection(new_connection, **kwargs) | ||||||
|  |  | ||||||
|  |         user = matching_users.first() | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.EMAIL_LINK, | ||||||
|  |             SourceUserMatchingModes.USERNAME_LINK, | ||||||
|  |         ]: | ||||||
|  |             new_connection.user = user | ||||||
|  |             new_connection = self.update_connection(new_connection, **kwargs) | ||||||
|  |             new_connection.save() | ||||||
|  |             return Action.LINK, new_connection | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.EMAIL_DENY, | ||||||
|  |             SourceUserMatchingModes.USERNAME_DENY, | ||||||
|  |         ]: | ||||||
|  |             self._logger.info("denying source because user exists", user=user) | ||||||
|  |             return Action.DENY, None | ||||||
|  |         # Should never get here as default enroll case is returned above. | ||||||
|  |         return Action.DENY, None | ||||||
|  |  | ||||||
|  |     def update_connection( | ||||||
|  |         self, connection: UserSourceConnection, **kwargs | ||||||
|  |     ) -> UserSourceConnection: | ||||||
|  |         """Optionally make changes to the connection after it is looked up/created.""" | ||||||
|  |         return connection | ||||||
|  |  | ||||||
|  |     def get_flow(self, **kwargs) -> HttpResponse: | ||||||
|  |         """Get the flow response based on user_matching_mode""" | ||||||
|  |         action, connection = self.get_action(**kwargs) | ||||||
|  |         self._logger.debug("get_action() says", action=action, connection=connection) | ||||||
|  |         if connection: | ||||||
|  |             if action == Action.LINK: | ||||||
|  |                 self._logger.debug("Linking existing user") | ||||||
|  |                 return self.handle_existing_user_link(connection) | ||||||
|  |             if action == Action.AUTH: | ||||||
|  |                 self._logger.debug("Handling auth user") | ||||||
|  |                 return self.handle_auth_user(connection) | ||||||
|  |             if action == Action.ENROLL: | ||||||
|  |                 self._logger.debug("Handling enrollment of new user") | ||||||
|  |                 return self.handle_enroll(connection) | ||||||
|  |         # Default case, assume deny | ||||||
|  |         messages.error( | ||||||
|  |             self.request, | ||||||
|  |             _( | ||||||
|  |                 ( | ||||||
|  |                     "Request to authenticate with %(source)s has been denied. Please authenticate " | ||||||
|  |                     "with the source you've previously signed up with." | ||||||
|  |                 ) | ||||||
|  |                 % {"source": self.source.name} | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         return redirect("/") | ||||||
|  |  | ||||||
|  |     # pylint: disable=unused-argument | ||||||
|  |     def get_stages_to_append(self, flow: Flow) -> list[Stage]: | ||||||
|  |         """Hook to override stages which are appended to the flow""" | ||||||
|  |         if flow.slug == self.source.enrollment_flow.slug: | ||||||
|  |             return [ | ||||||
|  |                 in_memory_stage(PostUserEnrollmentStage), | ||||||
|  |             ] | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |     def _handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse: | ||||||
|  |         """Prepare Authentication Plan, redirect user FlowExecutor""" | ||||||
|  |         # Ensure redirect is carried through when user was trying to | ||||||
|  |         # authorize application | ||||||
|  |         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||||
|  |             NEXT_ARG_NAME, "authentik_core:if-admin" | ||||||
|  |         ) | ||||||
|  |         kwargs.update( | ||||||
|  |             { | ||||||
|  |                 # Since we authenticate the user by their token, they have no backend set | ||||||
|  |                 PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend", | ||||||
|  |                 PLAN_CONTEXT_SSO: True, | ||||||
|  |                 PLAN_CONTEXT_SOURCE: self.source, | ||||||
|  |                 PLAN_CONTEXT_REDIRECT: final_redirect, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         if not flow: | ||||||
|  |             return HttpResponseBadRequest() | ||||||
|  |         # We run the Flow planner here so we can pass the Pending user in the context | ||||||
|  |         planner = FlowPlanner(flow) | ||||||
|  |         plan = planner.plan(self.request, kwargs) | ||||||
|  |         for stage in self.get_stages_to_append(flow): | ||||||
|  |             plan.append(stage) | ||||||
|  |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|  |         return redirect_with_qs( | ||||||
|  |             "authentik_core:if-flow", | ||||||
|  |             self.request.GET, | ||||||
|  |             flow_slug=flow.slug, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     # pylint: disable=unused-argument | ||||||
|  |     def handle_auth_user( | ||||||
|  |         self, | ||||||
|  |         connection: UserSourceConnection, | ||||||
|  |     ) -> HttpResponse: | ||||||
|  |         """Login user and redirect.""" | ||||||
|  |         messages.success( | ||||||
|  |             self.request, | ||||||
|  |             _( | ||||||
|  |                 "Successfully authenticated with %(source)s!" | ||||||
|  |                 % {"source": self.source.name} | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user} | ||||||
|  |         return self._handle_login_flow(self.source.authentication_flow, **flow_kwargs) | ||||||
|  |  | ||||||
|  |     def handle_existing_user_link( | ||||||
|  |         self, | ||||||
|  |         connection: UserSourceConnection, | ||||||
|  |     ) -> HttpResponse: | ||||||
|  |         """Handler when the user was already authenticated and linked an external source | ||||||
|  |         to their account.""" | ||||||
|  |         # Connection has already been saved | ||||||
|  |         Event.new( | ||||||
|  |             EventAction.SOURCE_LINKED, | ||||||
|  |             message="Linked Source", | ||||||
|  |             source=self.source, | ||||||
|  |         ).from_http(self.request) | ||||||
|  |         messages.success( | ||||||
|  |             self.request, | ||||||
|  |             _("Successfully linked %(source)s!" % {"source": self.source.name}), | ||||||
|  |         ) | ||||||
|  |         # When request isn't authenticated we jump straight to auth | ||||||
|  |         if not self.request.user.is_authenticated: | ||||||
|  |             return self.handle_auth_user(connection) | ||||||
|  |         return redirect( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_core:if-admin", | ||||||
|  |             ) | ||||||
|  |             + f"#/user;page-{self.source.slug}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def handle_enroll( | ||||||
|  |         self, | ||||||
|  |         connection: UserSourceConnection, | ||||||
|  |     ) -> HttpResponse: | ||||||
|  |         """User was not authenticated and previous request was not authenticated.""" | ||||||
|  |         messages.success( | ||||||
|  |             self.request, | ||||||
|  |             _( | ||||||
|  |                 "Successfully authenticated with %(source)s!" | ||||||
|  |                 % {"source": self.source.name} | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # We run the Flow planner here so we can pass the Pending user in the context | ||||||
|  |         if not self.source.enrollment_flow: | ||||||
|  |             self._logger.warning("source has no enrollment flow") | ||||||
|  |             return HttpResponseBadRequest() | ||||||
|  |         return self._handle_login_flow( | ||||||
|  |             self.source.enrollment_flow, | ||||||
|  |             **{ | ||||||
|  |                 PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info), | ||||||
|  |                 PLAN_CONTEXT_SOURCES_CONNECTION: connection, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
| @ -1,32 +1,30 @@ | |||||||
| """OAuth Stages""" | """Source flow manager stages""" | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| 
 | 
 | ||||||
| from authentik.core.models import User | from authentik.core.models import User, UserSourceConnection | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.sources.oauth.models import UserOAuthSourceConnection |  | ||||||
| 
 | 
 | ||||||
| PLAN_CONTEXT_SOURCES_OAUTH_ACCESS = "sources_oauth_access" | PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PostUserEnrollmentStage(StageView): | class PostUserEnrollmentStage(StageView): | ||||||
|     """Dynamically injected stage which saves the OAuth Connection after |     """Dynamically injected stage which saves the Connection after | ||||||
|     the user has been enrolled.""" |     the user has been enrolled.""" | ||||||
| 
 | 
 | ||||||
|     # pylint: disable=unused-argument |     # pylint: disable=unused-argument | ||||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: |     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|         """Stage used after the user has been enrolled""" |         """Stage used after the user has been enrolled""" | ||||||
|         access: UserOAuthSourceConnection = self.executor.plan.context[ |         connection: UserSourceConnection = self.executor.plan.context[ | ||||||
|             PLAN_CONTEXT_SOURCES_OAUTH_ACCESS |             PLAN_CONTEXT_SOURCES_CONNECTION | ||||||
|         ] |         ] | ||||||
|         user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] |         user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] | ||||||
|         access.user = user |         connection.user = user | ||||||
|         access.save() |         connection.save() | ||||||
|         UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user) |  | ||||||
|         Event.new( |         Event.new( | ||||||
|             EventAction.SOURCE_LINKED, |             EventAction.SOURCE_LINKED, | ||||||
|             message="Linked OAuth Source", |             message="Linked Source", | ||||||
|             source=access.source, |             source=connection.source, | ||||||
|         ).from_http(self.request) |         ).from_http(self.request) | ||||||
|         return self.executor.stage_ok() |         return self.executor.stage_ok() | ||||||
| @ -14,6 +14,8 @@ | |||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}"> | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}"> | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}"> | ||||||
|  |         {% block head_before %} | ||||||
|  |         {% endblock %} | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}"> | ||||||
|         <script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script> |         <script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script> | ||||||
|         <script>window["polymerSkipLoadingFontRoboto"] = true;</script> |         <script>window["polymerSkipLoadingFontRoboto"] = true;</script> | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ | |||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block body %} | {% block body %} | ||||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||||
|     <div class="pf-c-empty-state"> |     <div class="pf-c-empty-state"> | ||||||
|         <div class="pf-c-empty-state__content"> |         <div class="pf-c-empty-state__content"> | ||||||
|             <i class="fas fa-exclamation-circle pf-c-empty-state__icon" aria-hidden="true"></i> |             <i class="fas fa-exclamation-circle pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ | |||||||
| {% 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-initial-load pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> |     <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||||
|         <div class="pf-c-empty-state" style="height: 100vh;"> |         <div class="pf-c-empty-state" style="height: 100vh;"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <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 pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> | ||||||
|  | |||||||
| @ -3,6 +3,10 @@ | |||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block head_before %} | ||||||
|  | <script>ShadyDOM = { force: !navigator.webdriver };</script> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| {% block head %} | {% block head %} | ||||||
| <script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script> | <script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
| @ -10,7 +14,7 @@ | |||||||
| {% 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-initial-load pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> |     <section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"> | ||||||
|         <div class="pf-c-empty-state" style="height: 100vh;"> |         <div class="pf-c-empty-state" style="height: 100vh;"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <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 pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}"> | ||||||
|  | |||||||
| @ -3,6 +3,10 @@ | |||||||
| {% load static %} | {% load static %} | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block head_before %} | ||||||
|  | <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}"> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| {% block body %} | {% block body %} | ||||||
| <div class="pf-c-background-image"> | <div class="pf-c-background-image"> | ||||||
|     <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0"> |     <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0"> | ||||||
|  | |||||||
							
								
								
									
										125
									
								
								authentik/core/tests/test_applications_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								authentik/core/tests/test_applications_api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | |||||||
|  | """Test Applications API""" | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application, User | ||||||
|  | from authentik.policies.dummy.models import DummyPolicy | ||||||
|  | from authentik.policies.models import PolicyBinding | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestApplicationsAPI(APITestCase): | ||||||
|  |     """Test applications API""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         self.user = User.objects.get(username="akadmin") | ||||||
|  |         self.allowed = Application.objects.create(name="allowed", slug="allowed") | ||||||
|  |         self.denied = Application.objects.create(name="denied", slug="denied") | ||||||
|  |         PolicyBinding.objects.create( | ||||||
|  |             target=self.denied, | ||||||
|  |             policy=DummyPolicy.objects.create( | ||||||
|  |                 name="deny", result=False, wait_min=1, wait_max=2 | ||||||
|  |             ), | ||||||
|  |             order=0, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_check_access(self): | ||||||
|  |         """Test check_access operation """ | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_api:application-check-access", | ||||||
|  |                 kwargs={"slug": self.allowed.slug}, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 204) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_api:application-check-access", | ||||||
|  |                 kwargs={"slug": self.denied.slug}, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|  |     def test_list(self): | ||||||
|  |         """Test list operation without superuser_full_list""" | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         response = self.client.get(reverse("authentik_api:application-list")) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "pagination": { | ||||||
|  |                     "next": 0, | ||||||
|  |                     "previous": 0, | ||||||
|  |                     "count": 2, | ||||||
|  |                     "current": 1, | ||||||
|  |                     "total_pages": 1, | ||||||
|  |                     "start_index": 1, | ||||||
|  |                     "end_index": 2, | ||||||
|  |                 }, | ||||||
|  |                 "results": [ | ||||||
|  |                     { | ||||||
|  |                         "pk": str(self.allowed.pk), | ||||||
|  |                         "name": "allowed", | ||||||
|  |                         "slug": "allowed", | ||||||
|  |                         "provider": None, | ||||||
|  |                         "provider_obj": None, | ||||||
|  |                         "launch_url": None, | ||||||
|  |                         "meta_launch_url": "", | ||||||
|  |                         "meta_icon": None, | ||||||
|  |                         "meta_description": "", | ||||||
|  |                         "meta_publisher": "", | ||||||
|  |                         "policy_engine_mode": "any", | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_list_superuser_full_list(self): | ||||||
|  |         """Test list operation with superuser_full_list""" | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:application-list") + "?superuser_full_list=true" | ||||||
|  |         ) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "pagination": { | ||||||
|  |                     "next": 0, | ||||||
|  |                     "previous": 0, | ||||||
|  |                     "count": 2, | ||||||
|  |                     "current": 1, | ||||||
|  |                     "total_pages": 1, | ||||||
|  |                     "start_index": 1, | ||||||
|  |                     "end_index": 2, | ||||||
|  |                 }, | ||||||
|  |                 "results": [ | ||||||
|  |                     { | ||||||
|  |                         "pk": str(self.allowed.pk), | ||||||
|  |                         "name": "allowed", | ||||||
|  |                         "slug": "allowed", | ||||||
|  |                         "provider": None, | ||||||
|  |                         "provider_obj": None, | ||||||
|  |                         "launch_url": None, | ||||||
|  |                         "meta_launch_url": "", | ||||||
|  |                         "meta_icon": None, | ||||||
|  |                         "meta_description": "", | ||||||
|  |                         "meta_publisher": "", | ||||||
|  |                         "policy_engine_mode": "any", | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "launch_url": None, | ||||||
|  |                         "meta_description": "", | ||||||
|  |                         "meta_icon": None, | ||||||
|  |                         "meta_launch_url": "", | ||||||
|  |                         "meta_publisher": "", | ||||||
|  |                         "name": "denied", | ||||||
|  |                         "pk": str(self.denied.pk), | ||||||
|  |                         "policy_engine_mode": "any", | ||||||
|  |                         "provider": None, | ||||||
|  |                         "provider_obj": None, | ||||||
|  |                         "slug": "denied", | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
| @ -1,11 +1,14 @@ | |||||||
| """authentik core models tests""" | """authentik core models tests""" | ||||||
| from time import sleep | from time import sleep | ||||||
|  | from typing import Callable, Type | ||||||
|  |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils.timezone import now | from django.utils.timezone import now | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
|  |  | ||||||
| from authentik.core.models import Token | from authentik.core.models import Provider, Source, Token | ||||||
|  | from authentik.flows.models import Stage | ||||||
|  | from authentik.lib.utils.reflection import all_subclasses | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestModels(TestCase): | class TestModels(TestCase): | ||||||
| @ -24,3 +27,40 @@ class TestModels(TestCase): | |||||||
|         ) |         ) | ||||||
|         sleep(0.5) |         sleep(0.5) | ||||||
|         self.assertFalse(token.is_expired) |         self.assertFalse(token.is_expired) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def source_tester_factory(test_model: Type[Stage]) -> Callable: | ||||||
|  |     """Test source""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         model_class = None | ||||||
|  |         if test_model._meta.abstract: | ||||||
|  |             model_class = test_model.__bases__[0]() | ||||||
|  |         else: | ||||||
|  |             model_class = test_model() | ||||||
|  |         model_class.slug = "test" | ||||||
|  |         self.assertIsNotNone(model_class.component) | ||||||
|  |         _ = model_class.ui_login_button | ||||||
|  |         _ = model_class.ui_user_settings | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def provider_tester_factory(test_model: Type[Stage]) -> Callable: | ||||||
|  |     """Test provider""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         model_class = None | ||||||
|  |         if test_model._meta.abstract: | ||||||
|  |             model_class = test_model.__bases__[0]() | ||||||
|  |         else: | ||||||
|  |             model_class = test_model() | ||||||
|  |         self.assertIsNotNone(model_class.component) | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for model in all_subclasses(Source): | ||||||
|  |     setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model)) | ||||||
|  | for model in all_subclasses(Provider): | ||||||
|  |     setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model)) | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from django.urls.base import reverse | from django.urls.base import reverse | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import Token, User | from authentik.core.models import Token, TokenIntents, User | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestTokenAPI(APITestCase): | class TestTokenAPI(APITestCase): | ||||||
| @ -19,4 +19,6 @@ class TestTokenAPI(APITestCase): | |||||||
|             reverse("authentik_api:token-list"), {"identifier": "test-token"} |             reverse("authentik_api:token-list"), {"identifier": "test-token"} | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(response.status_code, 201) |         self.assertEqual(response.status_code, 201) | ||||||
|         self.assertEqual(Token.objects.get(identifier="test-token").user, self.user) |         token = Token.objects.get(identifier="test-token") | ||||||
|  |         self.assertEqual(token.user, self.user) | ||||||
|  |         self.assertEqual(token.intent, TokenIntents.INTENT_API) | ||||||
|  | |||||||
| @ -2,9 +2,10 @@ | |||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| from rest_framework.fields import CharField | from rest_framework.fields import CharField, DictField | ||||||
|  |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
|  | from authentik.flows.challenge import Challenge | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| @ -14,8 +15,8 @@ class UILoginButton: | |||||||
|     # Name, ran through i18n |     # Name, ran through i18n | ||||||
|     name: str |     name: str | ||||||
|  |  | ||||||
|     # URL Which Button points to |     # Challenge which is presented to the user when they click the button | ||||||
|     url: str |     challenge: Challenge | ||||||
|  |  | ||||||
|     # Icon URL, used as-is |     # Icon URL, used as-is | ||||||
|     icon_url: Optional[str] = None |     icon_url: Optional[str] = None | ||||||
| @ -25,7 +26,7 @@ class UILoginButtonSerializer(PassiveSerializer): | |||||||
|     """Serializer for Login buttons of sources""" |     """Serializer for Login buttons of sources""" | ||||||
|  |  | ||||||
|     name = CharField() |     name = CharField() | ||||||
|     url = CharField() |     challenge = DictField() | ||||||
|     icon_url = CharField(required=False, allow_null=True) |     icon_url = CharField(required=False, allow_null=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -39,7 +39,7 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|         """Show if this keypair has a private key configured or not""" |         """Show if this keypair has a private key configured or not""" | ||||||
|         return instance.key_data != "" and instance.key_data is not None |         return instance.key_data != "" and instance.key_data is not None | ||||||
|  |  | ||||||
|     def validate_certificate_data(self, value): |     def validate_certificate_data(self, value: str) -> str: | ||||||
|         """Verify that input is a valid PEM x509 Certificate""" |         """Verify that input is a valid PEM x509 Certificate""" | ||||||
|         try: |         try: | ||||||
|             load_pem_x509_certificate(value.encode("utf-8"), default_backend()) |             load_pem_x509_certificate(value.encode("utf-8"), default_backend()) | ||||||
| @ -47,7 +47,7 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|             raise ValidationError("Unable to load certificate.") |             raise ValidationError("Unable to load certificate.") | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
|     def validate_key_data(self, value): |     def validate_key_data(self, value: str) -> str: | ||||||
|         """Verify that input is a valid PEM RSA Key""" |         """Verify that input is a valid PEM RSA Key""" | ||||||
|         # Since this field is optional, data can be empty. |         # Since this field is optional, data can be empty. | ||||||
|         if value != "": |         if value != "": | ||||||
| @ -57,8 +57,10 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|                     password=None, |                     password=None, | ||||||
|                     backend=default_backend(), |                     backend=default_backend(), | ||||||
|                 ) |                 ) | ||||||
|             except ValueError: |             except (ValueError, TypeError): | ||||||
|                 raise ValidationError("Unable to load private key.") |                 raise ValidationError( | ||||||
|  |                     "Unable to load private key (possibly encrypted?)." | ||||||
|  |                 ) | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ class CertificateBuilder: | |||||||
|     def save(self) -> Optional[CertificateKeyPair]: |     def save(self) -> Optional[CertificateKeyPair]: | ||||||
|         """Save generated certificate as model""" |         """Save generated certificate as model""" | ||||||
|         if not self.__certificate: |         if not self.__certificate: | ||||||
|             return None |             raise ValueError("Certificated hasn't been built yet") | ||||||
|         return CertificateKeyPair.objects.create( |         return CertificateKeyPair.objects.create( | ||||||
|             name=self.common_name, |             name=self.common_name, | ||||||
|             certificate_data=self.certificate, |             certificate_data=self.certificate, | ||||||
|  | |||||||
| @ -37,6 +37,8 @@ class TestCrypto(TestCase): | |||||||
|         """Test Builder""" |         """Test Builder""" | ||||||
|         builder = CertificateBuilder() |         builder = CertificateBuilder() | ||||||
|         builder.common_name = "test-cert" |         builder.common_name = "test-cert" | ||||||
|  |         with self.assertRaises(ValueError): | ||||||
|  |             builder.save() | ||||||
|         builder.build( |         builder.build( | ||||||
|             subject_alt_names=[], |             subject_alt_names=[], | ||||||
|             validity_days=3, |             validity_days=3, | ||||||
|  | |||||||
| @ -8,10 +8,10 @@ from rest_framework.decorators import action | |||||||
| from rest_framework.fields import CharField, DictField, IntegerField | from rest_framework.fields import CharField, DictField, IntegerField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import ModelSerializer, Serializer | from rest_framework.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | from rest_framework.viewsets import ReadOnlyModelViewSet | ||||||
|  |  | ||||||
| from authentik.core.api.utils import TypeCreateSerializer | from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -38,31 +38,19 @@ class EventSerializer(ModelSerializer): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventTopPerUserParams(Serializer): | class EventTopPerUserParams(PassiveSerializer): | ||||||
|     """Query params for top_per_user""" |     """Query params for top_per_user""" | ||||||
|  |  | ||||||
|     top_n = IntegerField(default=15) |     top_n = IntegerField(default=15) | ||||||
|  |  | ||||||
|     def create(self, request: Request) -> Response: |  | ||||||
|         raise NotImplementedError |  | ||||||
|  |  | ||||||
|     def update(self, request: Request) -> Response: | class EventTopPerUserSerializer(PassiveSerializer): | ||||||
|         raise NotImplementedError |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventTopPerUserSerializer(Serializer): |  | ||||||
|     """Response object of Event's top_per_user""" |     """Response object of Event's top_per_user""" | ||||||
|  |  | ||||||
|     application = DictField() |     application = DictField() | ||||||
|     counted_events = IntegerField() |     counted_events = IntegerField() | ||||||
|     unique_users = IntegerField() |     unique_users = IntegerField() | ||||||
|  |  | ||||||
|     def create(self, request: Request) -> Response: |  | ||||||
|         raise NotImplementedError |  | ||||||
|  |  | ||||||
|     def update(self, request: Request) -> Response: |  | ||||||
|         raise NotImplementedError |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventsFilter(django_filters.FilterSet): | class EventsFilter(django_filters.FilterSet): | ||||||
|     """Filter for events""" |     """Filter for events""" | ||||||
| @ -132,7 +120,7 @@ class EventViewSet(ReadOnlyModelViewSet): | |||||||
|     def top_per_user(self, request: Request): |     def top_per_user(self, request: Request): | ||||||
|         """Get the top_n events grouped by user count""" |         """Get the top_n events grouped by user count""" | ||||||
|         filtered_action = request.query_params.get("action", EventAction.LOGIN) |         filtered_action = request.query_params.get("action", EventAction.LOGIN) | ||||||
|         top_n = request.query_params.get("top_n", 15) |         top_n = int(request.query_params.get("top_n", "15")) | ||||||
|         return Response( |         return Response( | ||||||
|             get_objects_for_user(request.user, "authentik_events.view_event") |             get_objects_for_user(request.user, "authentik_events.view_event") | ||||||
|             .filter(action=filtered_action) |             .filter(action=filtered_action) | ||||||
| @ -153,10 +141,6 @@ class EventViewSet(ReadOnlyModelViewSet): | |||||||
|         data = [] |         data = [] | ||||||
|         for value, name in EventAction.choices: |         for value, name in EventAction.choices: | ||||||
|             data.append( |             data.append( | ||||||
|                 { |                 {"name": name, "description": "", "component": value, "model_name": ""} | ||||||
|                     "name": name, |  | ||||||
|                     "description": "", |  | ||||||
|                     "component": value, |  | ||||||
|                 } |  | ||||||
|             ) |             ) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """Notification API Views""" | """Notification API Views""" | ||||||
|  | from guardian.utils import get_anonymous_user | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
| from rest_framework.fields import ReadOnlyField | from rest_framework.fields import ReadOnlyField | ||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| @ -48,6 +49,5 @@ class NotificationViewSet( | |||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         if not self.request: |         user = self.request.user if self.request else get_anonymous_user() | ||||||
|             return super().get_queryset() |         return Notification.objects.filter(user=user.pk) | ||||||
|         return Notification.objects.filter(user=self.request.user) |  | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| """Event notification tasks""" | """Event notification tasks""" | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
| from structlog import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.events.models import ( | from authentik.events.models import ( | ||||||
| @ -35,7 +35,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str): | |||||||
|         LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) |         LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) | ||||||
|         return |         return | ||||||
|     event: Event = events.first() |     event: Event = events.first() | ||||||
|     trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name) |     triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name) | ||||||
|  |     if not triggers.exists(): | ||||||
|  |         return | ||||||
|  |     trigger = triggers.first() | ||||||
|  |  | ||||||
|     if "policy_uuid" in event.context: |     if "policy_uuid" in event.context: | ||||||
|         policy_uuid = event.context["policy_uuid"] |         policy_uuid = event.context["policy_uuid"] | ||||||
| @ -58,7 +61,13 @@ def event_trigger_handler(event_uuid: str, trigger_name: str): | |||||||
|         return |         return | ||||||
|  |  | ||||||
|     LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger) |     LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger) | ||||||
|     user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user() |     try: | ||||||
|  |         user = ( | ||||||
|  |             User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user() | ||||||
|  |         ) | ||||||
|  |     except User.DoesNotExist: | ||||||
|  |         LOGGER.warning("e(trigger): failed to get user", trigger=trigger) | ||||||
|  |         return | ||||||
|     policy_engine = PolicyEngine(trigger, user) |     policy_engine = PolicyEngine(trigger, user) | ||||||
|     policy_engine.mode = PolicyEngineMode.MODE_ANY |     policy_engine.mode = PolicyEngineMode.MODE_ANY | ||||||
|     policy_engine.empty_result = False |     policy_engine.empty_result = False | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ from typing import Iterable | |||||||
| from drf_yasg.utils import swagger_auto_schema | from drf_yasg.utils import swagger_auto_schema | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
|  | from rest_framework.fields import BooleanField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import ModelSerializer, SerializerMethodField | from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||||
| @ -19,6 +20,12 @@ from authentik.lib.utils.reflection import all_subclasses | |||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class StageUserSettingSerializer(UserSettingSerializer): | ||||||
|  |     """User settings but can include a configure flow""" | ||||||
|  |  | ||||||
|  |     configure_flow = BooleanField(required=False) | ||||||
|  |  | ||||||
|  |  | ||||||
| class StageSerializer(ModelSerializer, MetaNameSerializer): | class StageSerializer(ModelSerializer, MetaNameSerializer): | ||||||
|     """Stage Serializer""" |     """Stage Serializer""" | ||||||
|  |  | ||||||
| @ -73,12 +80,13 @@ class StageViewSet( | |||||||
|                     "name": subclass._meta.verbose_name, |                     "name": subclass._meta.verbose_name, | ||||||
|                     "description": subclass.__doc__, |                     "description": subclass.__doc__, | ||||||
|                     "component": subclass().component, |                     "component": subclass().component, | ||||||
|  |                     "model_name": subclass._meta.model_name, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         data = sorted(data, key=lambda x: x["name"]) |         data = sorted(data, key=lambda x: x["name"]) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
|  |  | ||||||
|     @swagger_auto_schema(responses={200: UserSettingSerializer(many=True)}) |     @swagger_auto_schema(responses={200: StageUserSettingSerializer(many=True)}) | ||||||
|     @action(detail=False, pagination_class=None, filter_backends=[]) |     @action(detail=False, pagination_class=None, filter_backends=[]) | ||||||
|     def user_settings(self, request: Request) -> Response: |     def user_settings(self, request: Request) -> Response: | ||||||
|         """Get all stages the user can configure""" |         """Get all stages the user can configure""" | ||||||
| @ -89,6 +97,10 @@ class StageViewSet( | |||||||
|             if not user_settings: |             if not user_settings: | ||||||
|                 continue |                 continue | ||||||
|             user_settings.initial_data["object_uid"] = str(stage.pk) |             user_settings.initial_data["object_uid"] = str(stage.pk) | ||||||
|  |             if hasattr(stage, "configure_flow"): | ||||||
|  |                 user_settings.initial_data["configure_flow"] = bool( | ||||||
|  |                     stage.configure_flow | ||||||
|  |                 ) | ||||||
|             if not user_settings.is_valid(): |             if not user_settings.is_valid(): | ||||||
|                 LOGGER.warning(user_settings.errors) |                 LOGGER.warning(user_settings.errors) | ||||||
|             matching_stages.append(user_settings.initial_data) |             matching_stages.append(user_settings.initial_data) | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								authentik/flows/tests/test_stage_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								authentik/flows/tests/test_stage_model.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | """base model tests""" | ||||||
|  | from typing import Callable, Type | ||||||
|  |  | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.flows.models import Stage | ||||||
|  | from authentik.flows.stage import StageView | ||||||
|  | from authentik.lib.utils.reflection import all_subclasses | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestModels(TestCase): | ||||||
|  |     """Generic model properties tests""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def model_tester_factory(test_model: Type[Stage]) -> Callable: | ||||||
|  |     """Test a form""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         model_class = None | ||||||
|  |         if test_model._meta.abstract: | ||||||
|  |             model_class = test_model.__bases__[0]() | ||||||
|  |         else: | ||||||
|  |             model_class = test_model() | ||||||
|  |         self.assertTrue(issubclass(model_class.type, StageView)) | ||||||
|  |         self.assertIsNotNone(test_model.component) | ||||||
|  |         _ = test_model.ui_user_settings | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for model in all_subclasses(Stage): | ||||||
|  |     setattr(TestModels, f"test_model_{model.__name__}", model_tester_factory(model)) | ||||||
| @ -13,7 +13,7 @@ from django.db.models.query_utils import Q | |||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.serializers import BaseSerializer, Serializer | from rest_framework.serializers import BaseSerializer, Serializer | ||||||
| from structlog import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
|  |  | ||||||
| from authentik.flows.models import Flow, FlowStageBinding, Stage | from authentik.flows.models import Flow, FlowStageBinding, Stage | ||||||
| from authentik.flows.transfer.common import ( | from authentik.flows.transfer.common import ( | ||||||
| @ -160,7 +160,7 @@ class FlowImporter: | |||||||
|             try: |             try: | ||||||
|                 model: SerializerModel = apps.get_model(model_app_label, model_name) |                 model: SerializerModel = apps.get_model(model_app_label, model_name) | ||||||
|             except LookupError: |             except LookupError: | ||||||
|                 self.logger.error( |                 self.logger.warning( | ||||||
|                     "app or model does not exist", app=model_app_label, model=model_name |                     "app or model does not exist", app=model_app_label, model=model_name | ||||||
|                 ) |                 ) | ||||||
|                 return False |                 return False | ||||||
| @ -168,7 +168,7 @@ class FlowImporter: | |||||||
|             try: |             try: | ||||||
|                 serializer = self._validate_single(entry) |                 serializer = self._validate_single(entry) | ||||||
|             except EntryInvalidError as exc: |             except EntryInvalidError as exc: | ||||||
|                 self.logger.error("entry not valid", entry=entry, error=exc) |                 self.logger.warning("entry not valid", entry=entry, error=exc) | ||||||
|                 return False |                 return False | ||||||
|  |  | ||||||
|             model = serializer.save() |             model = serializer.save() | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ from drf_yasg import openapi | |||||||
| from drf_yasg.utils import no_body, swagger_auto_schema | from drf_yasg.utils import no_body, swagger_auto_schema | ||||||
| from rest_framework.permissions import AllowAny | from rest_framework.permissions import AllowAny | ||||||
| from rest_framework.views import APIView | from rest_framework.views import APIView | ||||||
|  | from sentry_sdk import capture_exception | ||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import USER_ATTRIBUTE_DEBUG | from authentik.core.models import USER_ATTRIBUTE_DEBUG | ||||||
| @ -127,6 +128,7 @@ class FlowExecutorView(APIView): | |||||||
|     @swagger_auto_schema( |     @swagger_auto_schema( | ||||||
|         responses={ |         responses={ | ||||||
|             200: Challenge(), |             200: Challenge(), | ||||||
|  |             404: "No Token found",  # This error can be raised by the email stage | ||||||
|         }, |         }, | ||||||
|         request_body=no_body, |         request_body=no_body, | ||||||
|         manual_parameters=[ |         manual_parameters=[ | ||||||
| @ -151,7 +153,8 @@ class FlowExecutorView(APIView): | |||||||
|             stage_response = self.current_stage_view.get(request, *args, **kwargs) |             stage_response = self.current_stage_view.get(request, *args, **kwargs) | ||||||
|             return to_stage_response(request, stage_response) |             return to_stage_response(request, stage_response) | ||||||
|         except Exception as exc:  # pylint: disable=broad-except |         except Exception as exc:  # pylint: disable=broad-except | ||||||
|             self._logger.exception(exc) |             capture_exception(exc) | ||||||
|  |             self._logger.warning(exc) | ||||||
|             return to_stage_response(request, FlowErrorResponse(request, exc)) |             return to_stage_response(request, FlowErrorResponse(request, exc)) | ||||||
|  |  | ||||||
|     @swagger_auto_schema( |     @swagger_auto_schema( | ||||||
| @ -179,7 +182,8 @@ class FlowExecutorView(APIView): | |||||||
|             stage_response = self.current_stage_view.post(request, *args, **kwargs) |             stage_response = self.current_stage_view.post(request, *args, **kwargs) | ||||||
|             return to_stage_response(request, stage_response) |             return to_stage_response(request, stage_response) | ||||||
|         except Exception as exc:  # pylint: disable=broad-except |         except Exception as exc:  # pylint: disable=broad-except | ||||||
|             self._logger.exception(exc) |             capture_exception(exc) | ||||||
|  |             self._logger.warning(exc) | ||||||
|             return to_stage_response(request, FlowErrorResponse(request, exc)) |             return to_stage_response(request, FlowErrorResponse(request, exc)) | ||||||
|  |  | ||||||
|     def _initiate_plan(self) -> FlowPlan: |     def _initiate_plan(self) -> FlowPlan: | ||||||
|  | |||||||
| @ -86,6 +86,13 @@ class ConfigLoader: | |||||||
|         url = urlparse(value) |         url = urlparse(value) | ||||||
|         if url.scheme == "env": |         if url.scheme == "env": | ||||||
|             value = os.getenv(url.netloc, url.query) |             value = os.getenv(url.netloc, url.query) | ||||||
|  |         if url.scheme == "file": | ||||||
|  |             try: | ||||||
|  |                 with open(url.netloc, "r") as _file: | ||||||
|  |                     value = _file.read() | ||||||
|  |             except OSError: | ||||||
|  |                 self._log("error", f"Failed to read config value from {url.netloc}") | ||||||
|  |                 value = url.query | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
|     def update_from_file(self, path: str): |     def update_from_file(self, path: str): | ||||||
| @ -163,6 +170,7 @@ class ConfigLoader: | |||||||
|         # Walk each component of the path |         # Walk each component of the path | ||||||
|         path_parts = path.split(sep) |         path_parts = path.split(sep) | ||||||
|         for comp in path_parts[:-1]: |         for comp in path_parts[:-1]: | ||||||
|  |             # pyright: reportGeneralTypeIssues=false | ||||||
|             if comp not in root: |             if comp not in root: | ||||||
|                 root[comp] = {} |                 root[comp] = {} | ||||||
|             root = root.get(comp) |             root = root.get(comp) | ||||||
|  | |||||||
| @ -5,6 +5,10 @@ postgresql: | |||||||
|   user: authentik |   user: authentik | ||||||
|   password: 'env://POSTGRES_PASSWORD' |   password: 'env://POSTGRES_PASSWORD' | ||||||
|  |  | ||||||
|  | web: | ||||||
|  |   listen: 0.0.0.0:9000 | ||||||
|  |   listen_tls: 0.0.0.0:9443 | ||||||
|  |  | ||||||
| redis: | redis: | ||||||
|   host: localhost |   host: localhost | ||||||
|   password: '' |   password: '' | ||||||
| @ -34,7 +38,10 @@ email: | |||||||
|   from: authentik@localhost |   from: authentik@localhost | ||||||
|  |  | ||||||
| outposts: | outposts: | ||||||
|   docker_image_base: "beryju/authentik" # this is prepended to -proxy:version |   # Placeholders: | ||||||
|  |   # %(type)s: Outpost type; proxy, ldap, etc | ||||||
|  |   # %(version)s: Current version; 2021.4.1 | ||||||
|  |   docker_image_base: "beryju/authentik-%(type)s:%(version)s" | ||||||
|  |  | ||||||
| authentik: | authentik: | ||||||
|   avatars: gravatar  # gravatar or none |   avatars: gravatar  # gravatar or none | ||||||
|  | |||||||
| @ -5,21 +5,39 @@ from aioredis.errors import ConnectionClosedError, ReplyError | |||||||
| from billiard.exceptions import WorkerLostError | from billiard.exceptions import WorkerLostError | ||||||
| from botocore.client import ClientError | from botocore.client import ClientError | ||||||
| from celery.exceptions import CeleryError | from celery.exceptions import CeleryError | ||||||
|  | from channels.middleware import BaseMiddleware | ||||||
| from channels_redis.core import ChannelFull | from channels_redis.core import ChannelFull | ||||||
| from django.core.exceptions import DisallowedHost, ValidationError | from django.core.exceptions import SuspiciousOperation, ValidationError | ||||||
| from django.db import InternalError, OperationalError, ProgrammingError | from django.db import InternalError, OperationalError, ProgrammingError | ||||||
|  | from django.http.response import Http404 | ||||||
| from django_redis.exceptions import ConnectionInterrupted | from django_redis.exceptions import ConnectionInterrupted | ||||||
| from docker.errors import DockerException | from docker.errors import DockerException | ||||||
| from ldap3.core.exceptions import LDAPException | from ldap3.core.exceptions import LDAPException | ||||||
| from redis.exceptions import ConnectionError as RedisConnectionError | from redis.exceptions import ConnectionError as RedisConnectionError | ||||||
| from redis.exceptions import RedisError, ResponseError | from redis.exceptions import RedisError, ResponseError | ||||||
| from rest_framework.exceptions import APIException | from rest_framework.exceptions import APIException | ||||||
|  | from sentry_sdk import Hub | ||||||
|  | from sentry_sdk.tracing import Transaction | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| from websockets.exceptions import WebSocketException | from websockets.exceptions import WebSocketException | ||||||
|  |  | ||||||
|  | from authentik.lib.utils.reflection import class_to_path | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SentryWSMiddleware(BaseMiddleware): | ||||||
|  |     """Sentry Websocket middleweare to set the transaction name based on | ||||||
|  |     consumer class path""" | ||||||
|  |  | ||||||
|  |     async def __call__(self, scope, receive, send): | ||||||
|  |         transaction: Optional[Transaction] = Hub.current.scope.transaction | ||||||
|  |         class_path = class_to_path(self.inner.consumer_class) | ||||||
|  |         if transaction: | ||||||
|  |             transaction.name = class_path | ||||||
|  |         return await self.inner(scope, receive, send) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SentryIgnoredException(Exception): | class SentryIgnoredException(Exception): | ||||||
|     """Base Class for all errors that are suppressed, and not sent to sentry.""" |     """Base Class for all errors that are suppressed, and not sent to sentry.""" | ||||||
|  |  | ||||||
| @ -36,7 +54,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|         OperationalError, |         OperationalError, | ||||||
|         InternalError, |         InternalError, | ||||||
|         ProgrammingError, |         ProgrammingError, | ||||||
|         DisallowedHost, |         SuspiciousOperation, | ||||||
|         ValidationError, |         ValidationError, | ||||||
|         # Redis errors |         # Redis errors | ||||||
|         RedisConnectionError, |         RedisConnectionError, | ||||||
| @ -61,6 +79,8 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|         LDAPException, |         LDAPException, | ||||||
|         # Docker errors |         # Docker errors | ||||||
|         DockerException, |         DockerException, | ||||||
|  |         # End-user errors | ||||||
|  |         Http404, | ||||||
|     ) |     ) | ||||||
|     if "exc_info" in hint: |     if "exc_info" in hint: | ||||||
|         _, exc_value, _ = hint["exc_info"] |         _, exc_value, _ = hint["exc_info"] | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								authentik/lib/tests/test_utils_reflection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								authentik/lib/tests/test_utils_reflection.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | """Test Reflection utils""" | ||||||
|  |  | ||||||
|  | from datetime import datetime | ||||||
|  |  | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.lib.utils.reflection import path_to_class | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestReflectionUtils(TestCase): | ||||||
|  |     """Test Reflection-utils""" | ||||||
|  |  | ||||||
|  |     def test_path_to_class(self): | ||||||
|  |         """Test path_to_class""" | ||||||
|  |         self.assertIsNone(path_to_class(None)) | ||||||
|  |         self.assertEqual(path_to_class("datetime.datetime"), datetime) | ||||||
| @ -3,6 +3,9 @@ from typing import Any, Optional | |||||||
|  |  | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
|  |  | ||||||
|  | OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | ||||||
|  | USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | ||||||
|  |  | ||||||
|  |  | ||||||
| def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]: | def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]: | ||||||
|     """Attempt to get the client's IP by checking common HTTP Headers. |     """Attempt to get the client's IP by checking common HTTP Headers. | ||||||
| @ -18,9 +21,27 @@ def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]: | |||||||
|     return None |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]: | ||||||
|  |     """Get the actual remote IP when set by an outpost. Only | ||||||
|  |     allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set | ||||||
|  |     to outpost""" | ||||||
|  |     if not hasattr(request, "user"): | ||||||
|  |         return None | ||||||
|  |     if not request.user.is_authenticated: | ||||||
|  |         return None | ||||||
|  |     if OUTPOST_REMOTE_IP_HEADER not in request.META: | ||||||
|  |         return None | ||||||
|  |     if request.user.attributes.get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): | ||||||
|  |         return None | ||||||
|  |     return request.META[OUTPOST_REMOTE_IP_HEADER] | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]: | def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]: | ||||||
|     """Attempt to get the client's IP by checking common HTTP Headers. |     """Attempt to get the client's IP by checking common HTTP Headers. | ||||||
|     Returns none if no IP Could be found""" |     Returns none if no IP Could be found""" | ||||||
|     if request: |     if request: | ||||||
|  |         override = _get_outpost_override_ip(request) | ||||||
|  |         if override: | ||||||
|  |             return override | ||||||
|         return _get_client_ip_from_meta(request.META) |         return _get_client_ip_from_meta(request.META) | ||||||
|     return None |     return None | ||||||
|  | |||||||
| @ -82,6 +82,7 @@ class ServiceConnectionViewSet( | |||||||
|                     "name": subclass._meta.verbose_name, |                     "name": subclass._meta.verbose_name, | ||||||
|                     "description": subclass.__doc__, |                     "description": subclass.__doc__, | ||||||
|                     "component": subclass().component, |                     "component": subclass().component, | ||||||
|  |                     "model_name": subclass._meta.model_name, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ class OutpostSerializer(ModelSerializer): | |||||||
|         fields = [ |         fields = [ | ||||||
|             "pk", |             "pk", | ||||||
|             "name", |             "name", | ||||||
|  |             "type", | ||||||
|             "providers", |             "providers", | ||||||
|             "providers_obj", |             "providers_obj", | ||||||
|             "service_connection", |             "service_connection", | ||||||
|  | |||||||
| @ -1,17 +1,8 @@ | |||||||
| """authentik outposts app config""" | """authentik outposts app config""" | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from os import R_OK, access |  | ||||||
| from os.path import expanduser |  | ||||||
| from pathlib import Path |  | ||||||
| from socket import gethostname |  | ||||||
| from urllib.parse import urlparse |  | ||||||
|  |  | ||||||
| import yaml |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
| from django.db import ProgrammingError | from django.db import ProgrammingError | ||||||
| from docker.constants import DEFAULT_UNIX_SOCKET |  | ||||||
| from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME |  | ||||||
| from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| @ -27,49 +18,8 @@ class AuthentikOutpostConfig(AppConfig): | |||||||
|     def ready(self): |     def ready(self): | ||||||
|         import_module("authentik.outposts.signals") |         import_module("authentik.outposts.signals") | ||||||
|         try: |         try: | ||||||
|             AuthentikOutpostConfig.init_local_connection() |             from authentik.outposts.tasks import outpost_local_connection | ||||||
|  |  | ||||||
|  |             outpost_local_connection.delay() | ||||||
|         except ProgrammingError: |         except ProgrammingError: | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def init_local_connection(): |  | ||||||
|         """Check if local kubernetes or docker connections should be created""" |  | ||||||
|         from authentik.outposts.models import ( |  | ||||||
|             DockerServiceConnection, |  | ||||||
|             KubernetesServiceConnection, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Explicitly check against token filename, as thats |  | ||||||
|         # only present when the integration is enabled |  | ||||||
|         if Path(SERVICE_TOKEN_FILENAME).exists(): |  | ||||||
|             LOGGER.debug("Detected in-cluster Kubernetes Config") |  | ||||||
|             if not KubernetesServiceConnection.objects.filter(local=True).exists(): |  | ||||||
|                 LOGGER.debug("Created Service Connection for in-cluster") |  | ||||||
|                 KubernetesServiceConnection.objects.create( |  | ||||||
|                     name="Local Kubernetes Cluster", local=True, kubeconfig={} |  | ||||||
|                 ) |  | ||||||
|         # For development, check for the existence of a kubeconfig file |  | ||||||
|         kubeconfig_path = expanduser(KUBE_CONFIG_DEFAULT_LOCATION) |  | ||||||
|         if Path(kubeconfig_path).exists(): |  | ||||||
|             LOGGER.debug("Detected kubeconfig") |  | ||||||
|             kubeconfig_local_name = f"k8s-{gethostname()}" |  | ||||||
|             if not KubernetesServiceConnection.objects.filter( |  | ||||||
|                 name=kubeconfig_local_name |  | ||||||
|             ).exists(): |  | ||||||
|                 LOGGER.debug("Creating kubeconfig Service Connection") |  | ||||||
|                 with open(kubeconfig_path, "r") as _kubeconfig: |  | ||||||
|                     KubernetesServiceConnection.objects.create( |  | ||||||
|                         name=kubeconfig_local_name, |  | ||||||
|                         kubeconfig=yaml.safe_load(_kubeconfig), |  | ||||||
|                     ) |  | ||||||
|         unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path |  | ||||||
|         socket = Path(unix_socket_path) |  | ||||||
|         if socket.exists() and access(socket, R_OK): |  | ||||||
|             LOGGER.debug("Detected local docker socket") |  | ||||||
|             if len(DockerServiceConnection.objects.filter(local=True)) == 0: |  | ||||||
|                 LOGGER.debug("Created Service Connection for docker") |  | ||||||
|                 DockerServiceConnection.objects.create( |  | ||||||
|                     name="Local Docker connection", |  | ||||||
|                     local=True, |  | ||||||
|                     url=unix_socket_path, |  | ||||||
|                 ) |  | ||||||
|  | |||||||
| @ -82,7 +82,8 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|             state.version = msg.args.get("version", None) |             state.version = msg.args.get("version", None) | ||||||
|         elif msg.instruction == WebsocketMessageInstruction.ACK: |         elif msg.instruction == WebsocketMessageInstruction.ACK: | ||||||
|             return |             return | ||||||
|         state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5) |         if state.version: | ||||||
|  |             state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5) | ||||||
|  |  | ||||||
|         response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK) |         response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK) | ||||||
|         self.send_json(asdict(response)) |         self.send_json(asdict(response)) | ||||||
|  | |||||||
| @ -1,9 +1,12 @@ | |||||||
| """Base Controller""" | """Base Controller""" | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| from structlog.testing import capture_logs | from structlog.testing import capture_logs | ||||||
|  |  | ||||||
|  | from authentik import __version__ | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.outposts.models import Outpost, OutpostServiceConnection | from authentik.outposts.models import Outpost, OutpostServiceConnection | ||||||
|  |  | ||||||
| @ -21,6 +24,7 @@ class DeploymentPort: | |||||||
|     port: int |     port: int | ||||||
|     name: str |     name: str | ||||||
|     protocol: str |     protocol: str | ||||||
|  |     inner_port: Optional[int] = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseController: | class BaseController: | ||||||
| @ -55,3 +59,8 @@ class BaseController: | |||||||
|     def get_static_deployment(self) -> str: |     def get_static_deployment(self) -> str: | ||||||
|         """Return a static deployment configuration""" |         """Return a static deployment configuration""" | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     def get_container_image(self) -> str: | ||||||
|  |         """Get container image to use for this outpost""" | ||||||
|  |         image_name_template: str = CONFIG.y("outposts.docker_image_base") | ||||||
|  |         return image_name_template % {"type": self.outpost.type, "version": __version__} | ||||||
|  | |||||||
| @ -8,7 +8,6 @@ from docker.models.containers import Container | |||||||
| from yaml import safe_dump | from yaml import safe_dump | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
| from authentik.lib.config import CONFIG |  | ||||||
| from authentik.outposts.controllers.base import BaseController, ControllerException | from authentik.outposts.controllers.base import BaseController, ControllerException | ||||||
| from authentik.outposts.models import ( | from authentik.outposts.models import ( | ||||||
|     DockerServiceConnection, |     DockerServiceConnection, | ||||||
| @ -60,15 +59,14 @@ class DockerController(BaseController): | |||||||
|             return self.client.containers.get(container_name), False |             return self.client.containers.get(container_name), False | ||||||
|         except NotFound: |         except NotFound: | ||||||
|             self.logger.info("Container does not exist, creating") |             self.logger.info("Container does not exist, creating") | ||||||
|             image_prefix = CONFIG.y("outposts.docker_image_base") |             image_name = self.get_container_image() | ||||||
|             image_name = f"{image_prefix}-{self.outpost.type}:{__version__}" |  | ||||||
|             self.client.images.pull(image_name) |             self.client.images.pull(image_name) | ||||||
|             container_args = { |             container_args = { | ||||||
|                 "image": image_name, |                 "image": image_name, | ||||||
|                 "name": f"authentik-proxy-{self.outpost.uuid.hex}", |                 "name": f"authentik-proxy-{self.outpost.uuid.hex}", | ||||||
|                 "detach": True, |                 "detach": True, | ||||||
|                 "ports": { |                 "ports": { | ||||||
|                     f"{port.port}/{port.protocol.lower()}": port.port |                     f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port | ||||||
|                     for port in self.deployment_ports |                     for port in self.deployment_ports | ||||||
|                 }, |                 }, | ||||||
|                 "environment": self._get_env(), |                 "environment": self._get_env(), | ||||||
| @ -134,7 +132,8 @@ class DockerController(BaseController): | |||||||
|     def down(self): |     def down(self): | ||||||
|         try: |         try: | ||||||
|             container, _ = self._get_container() |             container, _ = self._get_container() | ||||||
|             container.kill() |             if container.status == "running": | ||||||
|  |                 container.kill() | ||||||
|             container.remove() |             container.remove() | ||||||
|         except DockerException as exc: |         except DockerException as exc: | ||||||
|             raise ControllerException from exc |             raise ControllerException from exc | ||||||
| @ -142,15 +141,15 @@ class DockerController(BaseController): | |||||||
|     def get_static_deployment(self) -> str: |     def get_static_deployment(self) -> str: | ||||||
|         """Generate docker-compose yaml for proxy, version 3.5""" |         """Generate docker-compose yaml for proxy, version 3.5""" | ||||||
|         ports = [ |         ports = [ | ||||||
|             f"{port.port}:{port.port}/{port.protocol.lower()}" |             f"{port.port}:{port.inner_port or port.port}/{port.protocol.lower()}" | ||||||
|             for port in self.deployment_ports |             for port in self.deployment_ports | ||||||
|         ] |         ] | ||||||
|         image_prefix = CONFIG.y("outposts.docker_image_base") |         image_name = self.get_container_image() | ||||||
|         compose = { |         compose = { | ||||||
|             "version": "3.5", |             "version": "3.5", | ||||||
|             "services": { |             "services": { | ||||||
|                 f"authentik_{self.outpost.type}": { |                 f"authentik_{self.outpost.type}": { | ||||||
|                     "image": f"{image_prefix}-{self.outpost.type}:{__version__}", |                     "image": image_name, | ||||||
|                     "ports": ports, |                     "ports": ports, | ||||||
|                     "environment": { |                     "environment": { | ||||||
|                         "AUTHENTIK_HOST": self.outpost.config.authentik_host, |                         "AUTHENTIK_HOST": self.outpost.config.authentik_host, | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """Base Kubernetes Reconciler""" | """Base Kubernetes Reconciler""" | ||||||
| from typing import TYPE_CHECKING, Generic, TypeVar | from typing import TYPE_CHECKING, Generic, TypeVar | ||||||
|  |  | ||||||
|  | from django.utils.text import slugify | ||||||
| from kubernetes.client import V1ObjectMeta | from kubernetes.client import V1ObjectMeta | ||||||
| from kubernetes.client.models.v1_deployment import V1Deployment | from kubernetes.client.models.v1_deployment import V1Deployment | ||||||
| from kubernetes.client.models.v1_pod import V1Pod | from kubernetes.client.models.v1_pod import V1Pod | ||||||
| @ -29,6 +30,11 @@ class NeedsUpdate(ReconcileTrigger): | |||||||
|     """Exception to trigger an update to the Kubernetes Object""" |     """Exception to trigger an update to the Kubernetes Object""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Disabled(SentryIgnoredException): | ||||||
|  |     """Exception which can be thrown in a reconciler to signal than an | ||||||
|  |     object should not be created.""" | ||||||
|  |  | ||||||
|  |  | ||||||
| class KubernetesObjectReconciler(Generic[T]): | class KubernetesObjectReconciler(Generic[T]): | ||||||
|     """Base Kubernetes Reconciler, handles the basic logic.""" |     """Base Kubernetes Reconciler, handles the basic logic.""" | ||||||
|  |  | ||||||
| @ -37,17 +43,24 @@ class KubernetesObjectReconciler(Generic[T]): | |||||||
|     def __init__(self, controller: "KubernetesController"): |     def __init__(self, controller: "KubernetesController"): | ||||||
|         self.controller = controller |         self.controller = controller | ||||||
|         self.namespace = controller.outpost.config.kubernetes_namespace |         self.namespace = controller.outpost.config.kubernetes_namespace | ||||||
|         self.logger = get_logger() |         self.logger = get_logger().bind(type=self.__class__.__name__) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def name(self) -> str: |     def name(self) -> str: | ||||||
|         """Get the name of the object this reconciler manages""" |         """Get the name of the object this reconciler manages""" | ||||||
|         raise NotImplementedError |         return self.controller.outpost.config.object_naming_template % { | ||||||
|  |             "name": slugify(self.controller.outpost.name), | ||||||
|  |             "uuid": self.controller.outpost.uuid.hex, | ||||||
|  |         } | ||||||
|  |  | ||||||
|     def up(self): |     def up(self): | ||||||
|         """Create object if it doesn't exist, update if needed or recreate if needed.""" |         """Create object if it doesn't exist, update if needed or recreate if needed.""" | ||||||
|         current = None |         current = None | ||||||
|         reference = self.get_reference_object() |         try: | ||||||
|  |             reference = self.get_reference_object() | ||||||
|  |         except Disabled: | ||||||
|  |             self.logger.debug("Object not required") | ||||||
|  |             return | ||||||
|         try: |         try: | ||||||
|             try: |             try: | ||||||
|                 current = self.retrieve() |                 current = self.retrieve() | ||||||
| @ -58,7 +71,6 @@ class KubernetesObjectReconciler(Generic[T]): | |||||||
|                 self.logger.debug("Other unhandled error", exc=exc) |                 self.logger.debug("Other unhandled error", exc=exc) | ||||||
|                 raise exc |                 raise exc | ||||||
|             else: |             else: | ||||||
|                 self.logger.debug("Got current, running reconcile") |  | ||||||
|                 self.reconcile(current, reference) |                 self.reconcile(current, reference) | ||||||
|         except NeedsRecreate: |         except NeedsRecreate: | ||||||
|             self.logger.debug("Recreate requested") |             self.logger.debug("Recreate requested") | ||||||
| @ -67,16 +79,22 @@ class KubernetesObjectReconciler(Generic[T]): | |||||||
|                 self.delete(current) |                 self.delete(current) | ||||||
|             else: |             else: | ||||||
|                 self.logger.debug("No old found, creating") |                 self.logger.debug("No old found, creating") | ||||||
|             self.logger.debug("Created") |             self.logger.debug("Creating") | ||||||
|             self.create(reference) |             self.create(reference) | ||||||
|         except NeedsUpdate: |         except NeedsUpdate: | ||||||
|             self.logger.debug("Updating") |             self.logger.debug("Updating") | ||||||
|             self.update(current, reference) |             self.update(current, reference) | ||||||
|         else: |         else: | ||||||
|             self.logger.debug("Nothing to do...") |             self.logger.debug("Object is up-to-date.") | ||||||
|  |  | ||||||
|     def down(self): |     def down(self): | ||||||
|         """Delete object if found""" |         """Delete object if found""" | ||||||
|  |         # Call self.get_reference_object to check if we even need to do anything | ||||||
|  |         try: | ||||||
|  |             self.get_reference_object() | ||||||
|  |         except Disabled: | ||||||
|  |             self.logger.debug("Object not required") | ||||||
|  |             return | ||||||
|         try: |         try: | ||||||
|             current = self.retrieve() |             current = self.retrieve() | ||||||
|             self.delete(current) |             self.delete(current) | ||||||
| @ -120,7 +138,7 @@ class KubernetesObjectReconciler(Generic[T]): | |||||||
|             namespace=self.namespace, |             namespace=self.namespace, | ||||||
|             labels={ |             labels={ | ||||||
|                 "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}", |                 "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}", | ||||||
|                 "app.kubernetes.io/instance": self.controller.outpost.name, |                 "app.kubernetes.io/instance": slugify(self.controller.outpost.name), | ||||||
|                 "app.kubernetes.io/version": __version__, |                 "app.kubernetes.io/version": __version__, | ||||||
|                 "app.kubernetes.io/managed-by": "goauthentik.io", |                 "app.kubernetes.io/managed-by": "goauthentik.io", | ||||||
|                 "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex, |                 "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex, | ||||||
|  | |||||||
| @ -16,8 +16,6 @@ from kubernetes.client import ( | |||||||
|     V1SecretKeySelector, |     V1SecretKeySelector, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| from authentik import __version__ |  | ||||||
| from authentik.lib.config import CONFIG |  | ||||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | from authentik.outposts.controllers.base import FIELD_MANAGER | ||||||
| from authentik.outposts.controllers.k8s.base import ( | from authentik.outposts.controllers.k8s.base import ( | ||||||
|     KubernetesObjectReconciler, |     KubernetesObjectReconciler, | ||||||
| @ -39,10 +37,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|         self.api = AppsV1Api(controller.client) |         self.api = AppsV1Api(controller.client) | ||||||
|         self.outpost = self.controller.outpost |         self.outpost = self.controller.outpost | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def name(self) -> str: |  | ||||||
|         return f"authentik-outpost-{self.controller.outpost.uuid.hex}" |  | ||||||
|  |  | ||||||
|     def reconcile(self, current: V1Deployment, reference: V1Deployment): |     def reconcile(self, current: V1Deployment, reference: V1Deployment): | ||||||
|         super().reconcile(current, reference) |         super().reconcile(current, reference) | ||||||
|         if current.spec.replicas != reference.spec.replicas: |         if current.spec.replicas != reference.spec.replicas: | ||||||
| @ -68,14 +62,13 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|         for port in self.controller.deployment_ports: |         for port in self.controller.deployment_ports: | ||||||
|             container_ports.append( |             container_ports.append( | ||||||
|                 V1ContainerPort( |                 V1ContainerPort( | ||||||
|                     container_port=port.port, |                     container_port=port.inner_port or port.port, | ||||||
|                     name=port.name, |                     name=port.name, | ||||||
|                     protocol=port.protocol.upper(), |                     protocol=port.protocol.upper(), | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         meta = self.get_object_meta(name=self.name) |         meta = self.get_object_meta(name=self.name) | ||||||
|         secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api" |         image_name = self.controller.get_container_image() | ||||||
|         image_prefix = CONFIG.y("outposts.docker_image_base") |  | ||||||
|         return V1Deployment( |         return V1Deployment( | ||||||
|             metadata=meta, |             metadata=meta, | ||||||
|             spec=V1DeploymentSpec( |             spec=V1DeploymentSpec( | ||||||
| @ -87,14 +80,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|                         containers=[ |                         containers=[ | ||||||
|                             V1Container( |                             V1Container( | ||||||
|                                 name=str(self.outpost.type), |                                 name=str(self.outpost.type), | ||||||
|                                 image=f"{image_prefix}-{self.outpost.type}:{__version__}", |                                 image=image_name, | ||||||
|                                 ports=container_ports, |                                 ports=container_ports, | ||||||
|                                 env=[ |                                 env=[ | ||||||
|                                     V1EnvVar( |                                     V1EnvVar( | ||||||
|                                         name="AUTHENTIK_HOST", |                                         name="AUTHENTIK_HOST", | ||||||
|                                         value_from=V1EnvVarSource( |                                         value_from=V1EnvVarSource( | ||||||
|                                             secret_key_ref=V1SecretKeySelector( |                                             secret_key_ref=V1SecretKeySelector( | ||||||
|                                                 name=secret_name, |                                                 name=self.name, | ||||||
|                                                 key="authentik_host", |                                                 key="authentik_host", | ||||||
|                                             ) |                                             ) | ||||||
|                                         ), |                                         ), | ||||||
| @ -103,7 +96,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|                                         name="AUTHENTIK_TOKEN", |                                         name="AUTHENTIK_TOKEN", | ||||||
|                                         value_from=V1EnvVarSource( |                                         value_from=V1EnvVarSource( | ||||||
|                                             secret_key_ref=V1SecretKeySelector( |                                             secret_key_ref=V1SecretKeySelector( | ||||||
|                                                 name=secret_name, |                                                 name=self.name, | ||||||
|                                                 key="token", |                                                 key="token", | ||||||
|                                             ) |                                             ) | ||||||
|                                         ), |                                         ), | ||||||
| @ -112,7 +105,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|                                         name="AUTHENTIK_INSECURE", |                                         name="AUTHENTIK_INSECURE", | ||||||
|                                         value_from=V1EnvVarSource( |                                         value_from=V1EnvVarSource( | ||||||
|                                             secret_key_ref=V1SecretKeySelector( |                                             secret_key_ref=V1SecretKeySelector( | ||||||
|                                                 name=secret_name, |                                                 name=self.name, | ||||||
|                                                 key="authentik_host_insecure", |                                                 key="authentik_host_insecure", | ||||||
|                                             ) |                                             ) | ||||||
|                                         ), |                                         ), | ||||||
|  | |||||||
| @ -26,10 +26,6 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]): | |||||||
|         super().__init__(controller) |         super().__init__(controller) | ||||||
|         self.api = CoreV1Api(controller.client) |         self.api = CoreV1Api(controller.client) | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def name(self) -> str: |  | ||||||
|         return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api" |  | ||||||
|  |  | ||||||
|     def reconcile(self, current: V1Secret, reference: V1Secret): |     def reconcile(self, current: V1Secret, reference: V1Secret): | ||||||
|         super().reconcile(current, reference) |         super().reconcile(current, reference) | ||||||
|         for key in reference.data.keys(): |         for key in reference.data.keys(): | ||||||
|  | |||||||
| @ -21,10 +21,6 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]): | |||||||
|         super().__init__(controller) |         super().__init__(controller) | ||||||
|         self.api = CoreV1Api(controller.client) |         self.api = CoreV1Api(controller.client) | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def name(self) -> str: |  | ||||||
|         return f"authentik-outpost-{self.controller.outpost.uuid.hex}" |  | ||||||
|  |  | ||||||
|     def reconcile(self, current: V1Service, reference: V1Service): |     def reconcile(self, current: V1Service, reference: V1Service): | ||||||
|         super().reconcile(current, reference) |         super().reconcile(current, reference) | ||||||
|         if len(current.spec.ports) != len(reference.spec.ports): |         if len(current.spec.ports) != len(reference.spec.ports): | ||||||
| @ -43,13 +39,17 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]): | |||||||
|                     name=port.name, |                     name=port.name, | ||||||
|                     port=port.port, |                     port=port.port, | ||||||
|                     protocol=port.protocol.upper(), |                     protocol=port.protocol.upper(), | ||||||
|                     target_port=port.port, |                     target_port=port.inner_port or port.port, | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         selector_labels = DeploymentReconciler(self.controller).get_pod_meta() |         selector_labels = DeploymentReconciler(self.controller).get_pod_meta() | ||||||
|         return V1Service( |         return V1Service( | ||||||
|             metadata=meta, |             metadata=meta, | ||||||
|             spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"), |             spec=V1ServiceSpec( | ||||||
|  |                 ports=ports, | ||||||
|  |                 selector=selector_labels, | ||||||
|  |                 type=self.controller.outpost.config.kubernetes_service_type, | ||||||
|  |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def create(self, reference: V1Service): |     def create(self, reference: V1Service): | ||||||
|  | |||||||
| @ -2,14 +2,13 @@ | |||||||
| from io import StringIO | from io import StringIO | ||||||
| from typing import Type | from typing import Type | ||||||
|  |  | ||||||
| from kubernetes.client import OpenApiException |  | ||||||
| from kubernetes.client.api_client import ApiClient | from kubernetes.client.api_client import ApiClient | ||||||
|  | from kubernetes.client.exceptions import ApiException | ||||||
| from structlog.testing import capture_logs | from structlog.testing import capture_logs | ||||||
| from urllib3.exceptions import HTTPError |  | ||||||
| from yaml import dump_all | from yaml import dump_all | ||||||
|  |  | ||||||
| from authentik.outposts.controllers.base import BaseController, ControllerException | from authentik.outposts.controllers.base import BaseController, ControllerException | ||||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | from authentik.outposts.controllers.k8s.base import Disabled, KubernetesObjectReconciler | ||||||
| from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler | from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler | ||||||
| from authentik.outposts.controllers.k8s.secret import SecretReconciler | from authentik.outposts.controllers.k8s.secret import SecretReconciler | ||||||
| from authentik.outposts.controllers.k8s.service import ServiceReconciler | from authentik.outposts.controllers.k8s.service import ServiceReconciler | ||||||
| @ -43,8 +42,8 @@ class KubernetesController(BaseController): | |||||||
|                 reconciler = self.reconcilers[reconcile_key](self) |                 reconciler = self.reconcilers[reconcile_key](self) | ||||||
|                 reconciler.up() |                 reconciler.up() | ||||||
|  |  | ||||||
|         except (OpenApiException, HTTPError) as exc: |         except ApiException as exc: | ||||||
|             raise ControllerException from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def up_with_logs(self) -> list[str]: |     def up_with_logs(self) -> list[str]: | ||||||
|         try: |         try: | ||||||
| @ -55,8 +54,8 @@ class KubernetesController(BaseController): | |||||||
|                     reconciler.up() |                     reconciler.up() | ||||||
|                 all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] |                 all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] | ||||||
|             return all_logs |             return all_logs | ||||||
|         except (OpenApiException, HTTPError) as exc: |         except ApiException as exc: | ||||||
|             raise ControllerException from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def down(self): |     def down(self): | ||||||
|         try: |         try: | ||||||
| @ -64,14 +63,17 @@ class KubernetesController(BaseController): | |||||||
|                 reconciler = self.reconcilers[reconcile_key](self) |                 reconciler = self.reconcilers[reconcile_key](self) | ||||||
|                 reconciler.down() |                 reconciler.down() | ||||||
|  |  | ||||||
|         except OpenApiException as exc: |         except ApiException as exc: | ||||||
|             raise ControllerException from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def get_static_deployment(self) -> str: |     def get_static_deployment(self) -> str: | ||||||
|         documents = [] |         documents = [] | ||||||
|         for reconcile_key in self.reconcile_order: |         for reconcile_key in self.reconcile_order: | ||||||
|             reconciler = self.reconcilers[reconcile_key](self) |             reconciler = self.reconcilers[reconcile_key](self) | ||||||
|             documents.append(reconciler.get_reference_object().to_dict()) |             try: | ||||||
|  |                 documents.append(reconciler.get_reference_object().to_dict()) | ||||||
|  |             except Disabled: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|         with StringIO() as _str: |         with StringIO() as _str: | ||||||
|             dump_all( |             dump_all( | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								authentik/outposts/migrations/0016_alter_outpost_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								authentik/outposts/migrations/0016_alter_outpost_type.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-04-26 09:27 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_outposts", "0015_auto_20201224_1206"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="outpost", | ||||||
|  |             name="type", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[("proxy", "Proxy"), ("ldap", "Ldap")], default="proxy" | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -5,6 +5,7 @@ from typing import Iterable, Optional, Union | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from dacite import from_dict | from dacite import from_dict | ||||||
|  | from django.contrib.auth.models import Permission | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db import models, transaction | from django.db import models, transaction | ||||||
| from django.db.models.base import Model | from django.db.models.base import Model | ||||||
| @ -31,6 +32,7 @@ from authentik.crypto.models import CertificateKeyPair | |||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.models import InheritanceForeignKey | from authentik.lib.models import InheritanceForeignKey | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
|  | from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP | ||||||
| from authentik.outposts.docker_tls import DockerInlineTLS | from authentik.outposts.docker_tls import DockerInlineTLS | ||||||
|  |  | ||||||
| OUR_VERSION = parse(__version__) | OUR_VERSION = parse(__version__) | ||||||
| @ -55,16 +57,18 @@ class OutpostConfig: | |||||||
|         "error_reporting.environment", "customer" |         "error_reporting.environment", "customer" | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     object_naming_template: str = field(default="ak-outpost-%(name)s") | ||||||
|     kubernetes_replicas: int = field(default=1) |     kubernetes_replicas: int = field(default=1) | ||||||
|     kubernetes_namespace: str = field(default="default") |     kubernetes_namespace: str = field(default="default") | ||||||
|     kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) |     kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) | ||||||
|     kubernetes_ingress_secret_name: str = field(default="authentik-outpost") |     kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls") | ||||||
|  |     kubernetes_service_type: str = field(default="ClusterIP") | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostModel(Model): | class OutpostModel(Model): | ||||||
|     """Base model for providers that need more objects than just themselves""" |     """Base model for providers that need more objects than just themselves""" | ||||||
|  |  | ||||||
|     def get_required_objects(self) -> Iterable[models.Model]: |     def get_required_objects(self) -> Iterable[Union[models.Model, str]]: | ||||||
|         """Return a list of all required objects""" |         """Return a list of all required objects""" | ||||||
|         return [self] |         return [self] | ||||||
|  |  | ||||||
| @ -77,6 +81,7 @@ class OutpostType(models.TextChoices): | |||||||
|     """Outpost types, currently only the reverse proxy is available""" |     """Outpost types, currently only the reverse proxy is available""" | ||||||
|  |  | ||||||
|     PROXY = "proxy" |     PROXY = "proxy" | ||||||
|  |     LDAP = "ldap" | ||||||
|  |  | ||||||
|  |  | ||||||
| def default_outpost_config(host: Optional[str] = None): | def default_outpost_config(host: Optional[str] = None): | ||||||
| @ -201,7 +206,7 @@ class DockerServiceConnection(OutpostServiceConnection): | |||||||
|                 ) |                 ) | ||||||
|             client.containers.list() |             client.containers.list() | ||||||
|         except DockerException as exc: |         except DockerException as exc: | ||||||
|             LOGGER.error(exc) |             LOGGER.warning(exc) | ||||||
|             raise ServiceConnectionInvalid from exc |             raise ServiceConnectionInvalid from exc | ||||||
|         return client |         return client | ||||||
|  |  | ||||||
| @ -326,6 +331,7 @@ class Outpost(models.Model): | |||||||
|         if not users.exists(): |         if not users.exists(): | ||||||
|             user: User = User.objects.create(username=self.user_identifier) |             user: User = User.objects.create(username=self.user_identifier) | ||||||
|             user.attributes[USER_ATTRIBUTE_SA] = True |             user.attributes[USER_ATTRIBUTE_SA] = True | ||||||
|  |             user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True | ||||||
|             user.set_unusable_password() |             user.set_unusable_password() | ||||||
|             user.save() |             user.save() | ||||||
|         else: |         else: | ||||||
| @ -334,9 +340,29 @@ class Outpost(models.Model): | |||||||
|         # the ones the user needs |         # the ones the user needs | ||||||
|         with transaction.atomic(): |         with transaction.atomic(): | ||||||
|             UserObjectPermission.objects.filter(user=user).delete() |             UserObjectPermission.objects.filter(user=user).delete() | ||||||
|             for model in self.get_required_objects(): |             user.user_permissions.clear() | ||||||
|                 code_name = f"{model._meta.app_label}.view_{model._meta.model_name}" |             for model_or_perm in self.get_required_objects(): | ||||||
|                 assign_perm(code_name, user, model) |                 if isinstance(model_or_perm, models.Model): | ||||||
|  |                     model_or_perm: models.Model | ||||||
|  |                     code_name = ( | ||||||
|  |                         f"{model_or_perm._meta.app_label}." | ||||||
|  |                         f"view_{model_or_perm._meta.model_name}" | ||||||
|  |                     ) | ||||||
|  |                     assign_perm(code_name, user, model_or_perm) | ||||||
|  |                 else: | ||||||
|  |                     app_label, perm = model_or_perm.split(".") | ||||||
|  |                     permission = Permission.objects.filter( | ||||||
|  |                         codename=perm, | ||||||
|  |                         content_type__app_label=app_label, | ||||||
|  |                     ) | ||||||
|  |                     if not permission.exists(): | ||||||
|  |                         LOGGER.warning("permission doesn't exist", perm=model_or_perm) | ||||||
|  |                         continue | ||||||
|  |                     user.user_permissions.add(permission.first()) | ||||||
|  |         LOGGER.debug( | ||||||
|  |             "Updated service account's permissions", | ||||||
|  |             perms=UserObjectPermission.objects.filter(user=user), | ||||||
|  |         ) | ||||||
|         return user |         return user | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @ -356,12 +382,12 @@ class Outpost(models.Model): | |||||||
|             intent=TokenIntents.INTENT_API, |             intent=TokenIntents.INTENT_API, | ||||||
|             description=f"Autogenerated by authentik for Outpost {self.name}", |             description=f"Autogenerated by authentik for Outpost {self.name}", | ||||||
|             expiring=False, |             expiring=False, | ||||||
|             managed="goauthentik.io/outpost", |             managed=f"goauthentik.io/outpost/{self.token_identifier}", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def get_required_objects(self) -> Iterable[models.Model]: |     def get_required_objects(self) -> Iterable[Union[models.Model, str]]: | ||||||
|         """Get an iterator of all objects the user needs read access to""" |         """Get an iterator of all objects the user needs read access to""" | ||||||
|         objects = [self] |         objects: list[Union[models.Model, str]] = [self] | ||||||
|         for provider in ( |         for provider in ( | ||||||
|             Provider.objects.filter(outpost=self).select_related().select_subclasses() |             Provider.objects.filter(outpost=self).select_related().select_subclasses() | ||||||
|         ): |         ): | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|     }, |     }, | ||||||
|     "outposts_service_connection_check": { |     "outposts_service_connection_check": { | ||||||
|         "task": "authentik.outposts.tasks.outpost_service_connection_monitor", |         "task": "authentik.outposts.tasks.outpost_service_connection_monitor", | ||||||
|         "schedule": crontab(minute=0, hour="*"), |         "schedule": crontab(minute="*/60"), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
|     "outpost_token_ensurer": { |     "outpost_token_ensurer": { | ||||||
| @ -17,4 +17,9 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|         "schedule": crontab(minute="*/5"), |         "schedule": crontab(minute="*/5"), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
|  |     "outpost_local_connection": { | ||||||
|  |         "task": "authentik.outposts.tasks.outpost_local_connection", | ||||||
|  |         "schedule": crontab(minute="*/60"), | ||||||
|  |         "options": {"queue": "authentik_scheduled"}, | ||||||
|  |     }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,15 +1,16 @@ | |||||||
| """authentik outpost signals""" | """authentik outpost signals""" | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db.models import Model | from django.db.models import Model | ||||||
| from django.db.models.signals import post_save, pre_delete | from django.db.models.signals import post_save, pre_delete, pre_save | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Provider | from authentik.core.models import Provider | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.lib.utils.reflection import class_to_path | from authentik.lib.utils.reflection import class_to_path | ||||||
|  | from authentik.outposts.controllers.base import ControllerException | ||||||
| from authentik.outposts.models import Outpost, OutpostServiceConnection | from authentik.outposts.models import Outpost, OutpostServiceConnection | ||||||
| from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete | from authentik.outposts.tasks import outpost_controller_down, outpost_post_save | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| UPDATE_TRIGGERING_MODELS = ( | UPDATE_TRIGGERING_MODELS = ( | ||||||
| @ -20,6 +21,27 @@ UPDATE_TRIGGERING_MODELS = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(pre_save, sender=Outpost) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def pre_save_outpost(sender, instance: Outpost, **_): | ||||||
|  |     """Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes, | ||||||
|  |     we call down and then wait for the up after save""" | ||||||
|  |     old_instances = Outpost.objects.filter(pk=instance.pk) | ||||||
|  |     if not old_instances.exists(): | ||||||
|  |         return | ||||||
|  |     old_instance = old_instances.first() | ||||||
|  |     dirty = False | ||||||
|  |     # Name changes the deployment name, need to recreate | ||||||
|  |     dirty += old_instance.name != instance.name | ||||||
|  |     # namespace requires re-create | ||||||
|  |     dirty += ( | ||||||
|  |         old_instance.config.kubernetes_namespace != instance.config.kubernetes_namespace | ||||||
|  |     ) | ||||||
|  |     if bool(dirty): | ||||||
|  |         LOGGER.info("Outpost needs re-deployment due to changes", instance=instance) | ||||||
|  |         outpost_controller_down_wrapper(old_instance) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save) | @receiver(post_save) | ||||||
| # pylint: disable=unused-argument | # pylint: disable=unused-argument | ||||||
| def post_save_update(sender, instance: Model, **_): | def post_save_update(sender, instance: Model, **_): | ||||||
| @ -41,15 +63,23 @@ def post_save_update(sender, instance: Model, **_): | |||||||
| def pre_delete_cleanup(sender, instance: Outpost, **_): | def pre_delete_cleanup(sender, instance: Outpost, **_): | ||||||
|     """Ensure that Outpost's user is deleted (which will delete the token through cascade)""" |     """Ensure that Outpost's user is deleted (which will delete the token through cascade)""" | ||||||
|     instance.user.delete() |     instance.user.delete() | ||||||
|     # To ensure that deployment is cleaned up *consistently* we call the controller, and wait |     outpost_controller_down_wrapper(instance) | ||||||
|     # for it to finish. We don't want to call it in this thread, as we don't have the Outpost |  | ||||||
|     # Service connection here |  | ||||||
|  | def outpost_controller_down_wrapper(instance: Outpost): | ||||||
|  |     """To ensure that deployment is cleaned up *consistently* we call the controller, and wait | ||||||
|  |     for it to finish. We don't want to call it in this thread, as we don't have the Outpost | ||||||
|  |     Service connection here""" | ||||||
|     try: |     try: | ||||||
|         outpost_pre_delete.delay(instance.pk.hex).get() |         outpost_controller_down.delay(instance.pk.hex).get() | ||||||
|     except RuntimeError: |     except RuntimeError:  # pragma: no cover | ||||||
|         # In e2e/integration tests, this might run inside a thread/process and |         # In e2e/integration tests, this might run inside a thread/process and | ||||||
|         # trigger the celery `Never call result.get() within a task` detection |         # trigger the celery `Never call result.get() within a task` detection | ||||||
|         if settings.TEST: |         if settings.TEST: | ||||||
|             pass |             pass | ||||||
|         else: |         else: | ||||||
|             raise |             raise | ||||||
|  |     except ControllerException as exc: | ||||||
|  |         LOGGER.warning( | ||||||
|  |             "failed to cleanup outpost deployment", exc=exc, instance=instance | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -1,16 +1,25 @@ | |||||||
| """outpost tasks""" | """outpost tasks""" | ||||||
| from typing import Any | from os import R_OK, access | ||||||
|  | from os.path import expanduser | ||||||
|  | from pathlib import Path | ||||||
|  | from socket import gethostname | ||||||
|  | from typing import Any, Optional | ||||||
|  | from urllib.parse import urlparse | ||||||
|  |  | ||||||
|  | import yaml | ||||||
| from asgiref.sync import async_to_sync | from asgiref.sync import async_to_sync | ||||||
| from channels.layers import get_channel_layer | from channels.layers import get_channel_layer | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db.models.base import Model | from django.db.models.base import Model | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
|  | from docker.constants import DEFAULT_UNIX_SOCKET | ||||||
|  | from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME | ||||||
|  | from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
| from authentik.lib.utils.reflection import path_to_class | from authentik.lib.utils.reflection import path_to_class | ||||||
| from authentik.outposts.controllers.base import ControllerException | from authentik.outposts.controllers.base import BaseController, ControllerException | ||||||
| from authentik.outposts.models import ( | from authentik.outposts.models import ( | ||||||
|     DockerServiceConnection, |     DockerServiceConnection, | ||||||
|     KubernetesServiceConnection, |     KubernetesServiceConnection, | ||||||
| @ -20,6 +29,8 @@ from authentik.outposts.models import ( | |||||||
|     OutpostState, |     OutpostState, | ||||||
|     OutpostType, |     OutpostType, | ||||||
| ) | ) | ||||||
|  | from authentik.providers.ldap.controllers.docker import LDAPDockerController | ||||||
|  | from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController | ||||||
| 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 | ||||||
| @ -27,6 +38,24 @@ from authentik.root.celery import CELERY_APP | |||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def controller_for_outpost(outpost: Outpost) -> Optional[BaseController]: | ||||||
|  |     """Get a controller for the outpost, when a service connection is defined""" | ||||||
|  |     if not outpost.service_connection: | ||||||
|  |         return None | ||||||
|  |     service_connection = outpost.service_connection | ||||||
|  |     if outpost.type == OutpostType.PROXY: | ||||||
|  |         if isinstance(service_connection, DockerServiceConnection): | ||||||
|  |             return ProxyDockerController(outpost, service_connection) | ||||||
|  |         if isinstance(service_connection, KubernetesServiceConnection): | ||||||
|  |             return ProxyKubernetesController(outpost, service_connection) | ||||||
|  |     if outpost.type == OutpostType.LDAP: | ||||||
|  |         if isinstance(service_connection, DockerServiceConnection): | ||||||
|  |             return LDAPDockerController(outpost, service_connection) | ||||||
|  |         if isinstance(service_connection, KubernetesServiceConnection): | ||||||
|  |             return LDAPKubernetesController(outpost, service_connection) | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task() | @CELERY_APP.task() | ||||||
| def outpost_controller_all(): | def outpost_controller_all(): | ||||||
|     """Launch Controller for all Outposts which support it""" |     """Launch Controller for all Outposts which support it""" | ||||||
| @ -67,14 +96,10 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str): | |||||||
|     outpost: Outpost = Outpost.objects.get(pk=outpost_pk) |     outpost: Outpost = Outpost.objects.get(pk=outpost_pk) | ||||||
|     self.set_uid(slugify(outpost.name)) |     self.set_uid(slugify(outpost.name)) | ||||||
|     try: |     try: | ||||||
|         if outpost.type == OutpostType.PROXY: |         controller = controller_for_outpost(outpost) | ||||||
|             service_connection = outpost.service_connection |         if not controller: | ||||||
|             if isinstance(service_connection, DockerServiceConnection): |             return | ||||||
|                 logs = ProxyDockerController(outpost, service_connection).up_with_logs() |         logs = controller.up_with_logs() | ||||||
|             if isinstance(service_connection, KubernetesServiceConnection): |  | ||||||
|                 logs = ProxyKubernetesController( |  | ||||||
|                     outpost, service_connection |  | ||||||
|                 ).up_with_logs() |  | ||||||
|         LOGGER.debug("---------------Outpost Controller logs starting----------------") |         LOGGER.debug("---------------Outpost Controller logs starting----------------") | ||||||
|         for log in logs: |         for log in logs: | ||||||
|             LOGGER.debug(log) |             LOGGER.debug(log) | ||||||
| @ -86,15 +111,13 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str): | |||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task() | @CELERY_APP.task() | ||||||
| def outpost_pre_delete(outpost_pk: str): | def outpost_controller_down(outpost_pk: str): | ||||||
|     """Delete outpost objects before deleting the DB Object""" |     """Delete outpost objects before deleting the DB Object""" | ||||||
|     outpost = Outpost.objects.get(pk=outpost_pk) |     outpost = Outpost.objects.get(pk=outpost_pk) | ||||||
|     if outpost.type == OutpostType.PROXY: |     controller = controller_for_outpost(outpost) | ||||||
|         service_connection = outpost.service_connection |     if not controller: | ||||||
|         if isinstance(service_connection, DockerServiceConnection): |         return | ||||||
|             ProxyDockerController(outpost, service_connection).down() |     controller.down() | ||||||
|         if isinstance(service_connection, KubernetesServiceConnection): |  | ||||||
|             ProxyKubernetesController(outpost, service_connection).down() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| @ -183,3 +206,42 @@ def _outpost_single_update(outpost: Outpost, layer=None): | |||||||
|     for state in OutpostState.for_outpost(outpost): |     for state in OutpostState.for_outpost(outpost): | ||||||
|         LOGGER.debug("sending update", channel=state.uid, outpost=outpost) |         LOGGER.debug("sending update", channel=state.uid, outpost=outpost) | ||||||
|         async_to_sync(layer.send)(state.uid, {"type": "event.update"}) |         async_to_sync(layer.send)(state.uid, {"type": "event.update"}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task() | ||||||
|  | def outpost_local_connection(): | ||||||
|  |     """Checks the local environment and create Service connections.""" | ||||||
|  |     # Explicitly check against token filename, as thats | ||||||
|  |     # only present when the integration is enabled | ||||||
|  |     if Path(SERVICE_TOKEN_FILENAME).exists(): | ||||||
|  |         LOGGER.debug("Detected in-cluster Kubernetes Config") | ||||||
|  |         if not KubernetesServiceConnection.objects.filter(local=True).exists(): | ||||||
|  |             LOGGER.debug("Created Service Connection for in-cluster") | ||||||
|  |             KubernetesServiceConnection.objects.create( | ||||||
|  |                 name="Local Kubernetes Cluster", local=True, kubeconfig={} | ||||||
|  |             ) | ||||||
|  |     # For development, check for the existence of a kubeconfig file | ||||||
|  |     kubeconfig_path = expanduser(KUBE_CONFIG_DEFAULT_LOCATION) | ||||||
|  |     if Path(kubeconfig_path).exists(): | ||||||
|  |         LOGGER.debug("Detected kubeconfig") | ||||||
|  |         kubeconfig_local_name = f"k8s-{gethostname()}" | ||||||
|  |         if not KubernetesServiceConnection.objects.filter( | ||||||
|  |             name=kubeconfig_local_name | ||||||
|  |         ).exists(): | ||||||
|  |             LOGGER.debug("Creating kubeconfig Service Connection") | ||||||
|  |             with open(kubeconfig_path, "r") as _kubeconfig: | ||||||
|  |                 KubernetesServiceConnection.objects.create( | ||||||
|  |                     name=kubeconfig_local_name, | ||||||
|  |                     kubeconfig=yaml.safe_load(_kubeconfig), | ||||||
|  |                 ) | ||||||
|  |     unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path | ||||||
|  |     socket = Path(unix_socket_path) | ||||||
|  |     if socket.exists() and access(socket, R_OK): | ||||||
|  |         LOGGER.debug("Detected local docker socket") | ||||||
|  |         if len(DockerServiceConnection.objects.filter(local=True)) == 0: | ||||||
|  |             LOGGER.debug("Created Service Connection for docker") | ||||||
|  |             DockerServiceConnection.objects.create( | ||||||
|  |                 name="Local Docker connection", | ||||||
|  |                 local=True, | ||||||
|  |                 url=unix_socket_path, | ||||||
|  |             ) | ||||||
|  | |||||||
| @ -108,6 +108,7 @@ class PolicyViewSet( | |||||||
|                     "name": subclass._meta.verbose_name, |                     "name": subclass._meta.verbose_name, | ||||||
|                     "description": subclass.__doc__, |                     "description": subclass.__doc__, | ||||||
|                     "component": subclass().component, |                     "component": subclass().component, | ||||||
|  |                     "model_name": subclass._meta.model_name, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         return Response(TypeCreateSerializer(data, many=True).data) |         return Response(TypeCreateSerializer(data, many=True).data) | ||||||
|  | |||||||
| @ -0,0 +1,84 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-05-02 17:06 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_policies_event_matcher", "0012_auto_20210323_1339"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="eventmatcherpolicy", | ||||||
|  |             name="app", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 blank=True, | ||||||
|  |                 choices=[ | ||||||
|  |                     ("authentik.admin", "authentik Admin"), | ||||||
|  |                     ("authentik.api", "authentik API"), | ||||||
|  |                     ("authentik.events", "authentik Events"), | ||||||
|  |                     ("authentik.crypto", "authentik Crypto"), | ||||||
|  |                     ("authentik.flows", "authentik Flows"), | ||||||
|  |                     ("authentik.outposts", "authentik Outpost"), | ||||||
|  |                     ("authentik.lib", "authentik lib"), | ||||||
|  |                     ("authentik.policies", "authentik Policies"), | ||||||
|  |                     ("authentik.policies.dummy", "authentik Policies.Dummy"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.policies.event_matcher", | ||||||
|  |                         "authentik Policies.Event Matcher", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.policies.expiry", "authentik Policies.Expiry"), | ||||||
|  |                     ("authentik.policies.expression", "authentik Policies.Expression"), | ||||||
|  |                     ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"), | ||||||
|  |                     ("authentik.policies.password", "authentik Policies.Password"), | ||||||
|  |                     ("authentik.policies.reputation", "authentik Policies.Reputation"), | ||||||
|  |                     ("authentik.providers.proxy", "authentik Providers.Proxy"), | ||||||
|  |                     ("authentik.providers.oauth2", "authentik Providers.OAuth2"), | ||||||
|  |                     ("authentik.providers.saml", "authentik Providers.SAML"), | ||||||
|  |                     ("authentik.recovery", "authentik Recovery"), | ||||||
|  |                     ("authentik.sources.ldap", "authentik Sources.LDAP"), | ||||||
|  |                     ("authentik.sources.oauth", "authentik Sources.OAuth"), | ||||||
|  |                     ("authentik.sources.plex", "authentik Sources.Plex"), | ||||||
|  |                     ("authentik.sources.saml", "authentik Sources.SAML"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_static", | ||||||
|  |                         "authentik Stages.Authenticator.Static", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_totp", | ||||||
|  |                         "authentik Stages.Authenticator.TOTP", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_validate", | ||||||
|  |                         "authentik Stages.Authenticator.Validate", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_webauthn", | ||||||
|  |                         "authentik Stages.Authenticator.WebAuthn", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.stages.captcha", "authentik Stages.Captcha"), | ||||||
|  |                     ("authentik.stages.consent", "authentik Stages.Consent"), | ||||||
|  |                     ("authentik.stages.deny", "authentik Stages.Deny"), | ||||||
|  |                     ("authentik.stages.dummy", "authentik Stages.Dummy"), | ||||||
|  |                     ("authentik.stages.email", "authentik Stages.Email"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.identification", | ||||||
|  |                         "authentik Stages.Identification", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.stages.invitation", "authentik Stages.User Invitation"), | ||||||
|  |                     ("authentik.stages.password", "authentik Stages.Password"), | ||||||
|  |                     ("authentik.stages.prompt", "authentik Stages.Prompt"), | ||||||
|  |                     ("authentik.stages.user_delete", "authentik Stages.User Delete"), | ||||||
|  |                     ("authentik.stages.user_login", "authentik Stages.User Login"), | ||||||
|  |                     ("authentik.stages.user_logout", "authentik Stages.User Logout"), | ||||||
|  |                     ("authentik.stages.user_write", "authentik Stages.User Write"), | ||||||
|  |                     ("authentik.core", "authentik Core"), | ||||||
|  |                     ("authentik.managed", "authentik Managed"), | ||||||
|  |                 ], | ||||||
|  |                 default="", | ||||||
|  |                 help_text="Match events created by selected application. When left empty, all applications are matched.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,85 @@ | |||||||
|  | # Generated by Django 3.2.1 on 2021-05-05 17:17 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_policies_event_matcher", "0013_alter_eventmatcherpolicy_app"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="eventmatcherpolicy", | ||||||
|  |             name="app", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 blank=True, | ||||||
|  |                 choices=[ | ||||||
|  |                     ("authentik.admin", "authentik Admin"), | ||||||
|  |                     ("authentik.api", "authentik API"), | ||||||
|  |                     ("authentik.events", "authentik Events"), | ||||||
|  |                     ("authentik.crypto", "authentik Crypto"), | ||||||
|  |                     ("authentik.flows", "authentik Flows"), | ||||||
|  |                     ("authentik.outposts", "authentik Outpost"), | ||||||
|  |                     ("authentik.lib", "authentik lib"), | ||||||
|  |                     ("authentik.policies", "authentik Policies"), | ||||||
|  |                     ("authentik.policies.dummy", "authentik Policies.Dummy"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.policies.event_matcher", | ||||||
|  |                         "authentik Policies.Event Matcher", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.policies.expiry", "authentik Policies.Expiry"), | ||||||
|  |                     ("authentik.policies.expression", "authentik Policies.Expression"), | ||||||
|  |                     ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"), | ||||||
|  |                     ("authentik.policies.password", "authentik Policies.Password"), | ||||||
|  |                     ("authentik.policies.reputation", "authentik Policies.Reputation"), | ||||||
|  |                     ("authentik.providers.proxy", "authentik Providers.Proxy"), | ||||||
|  |                     ("authentik.providers.ldap", "authentik Providers.LDAP"), | ||||||
|  |                     ("authentik.providers.oauth2", "authentik Providers.OAuth2"), | ||||||
|  |                     ("authentik.providers.saml", "authentik Providers.SAML"), | ||||||
|  |                     ("authentik.recovery", "authentik Recovery"), | ||||||
|  |                     ("authentik.sources.ldap", "authentik Sources.LDAP"), | ||||||
|  |                     ("authentik.sources.oauth", "authentik Sources.OAuth"), | ||||||
|  |                     ("authentik.sources.plex", "authentik Sources.Plex"), | ||||||
|  |                     ("authentik.sources.saml", "authentik Sources.SAML"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_static", | ||||||
|  |                         "authentik Stages.Authenticator.Static", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_totp", | ||||||
|  |                         "authentik Stages.Authenticator.TOTP", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_validate", | ||||||
|  |                         "authentik Stages.Authenticator.Validate", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_webauthn", | ||||||
|  |                         "authentik Stages.Authenticator.WebAuthn", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.stages.captcha", "authentik Stages.Captcha"), | ||||||
|  |                     ("authentik.stages.consent", "authentik Stages.Consent"), | ||||||
|  |                     ("authentik.stages.deny", "authentik Stages.Deny"), | ||||||
|  |                     ("authentik.stages.dummy", "authentik Stages.Dummy"), | ||||||
|  |                     ("authentik.stages.email", "authentik Stages.Email"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.identification", | ||||||
|  |                         "authentik Stages.Identification", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.stages.invitation", "authentik Stages.User Invitation"), | ||||||
|  |                     ("authentik.stages.password", "authentik Stages.Password"), | ||||||
|  |                     ("authentik.stages.prompt", "authentik Stages.Prompt"), | ||||||
|  |                     ("authentik.stages.user_delete", "authentik Stages.User Delete"), | ||||||
|  |                     ("authentik.stages.user_login", "authentik Stages.User Login"), | ||||||
|  |                     ("authentik.stages.user_logout", "authentik Stages.User Logout"), | ||||||
|  |                     ("authentik.stages.user_write", "authentik Stages.User Write"), | ||||||
|  |                     ("authentik.core", "authentik Core"), | ||||||
|  |                     ("authentik.managed", "authentik Managed"), | ||||||
|  |                 ], | ||||||
|  |                 default="", | ||||||
|  |                 help_text="Match events created by selected application. When left empty, all applications are matched.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -4,7 +4,7 @@ | |||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  |  | ||||||
| {% block title %} | {% block title %} | ||||||
| {% trans 'Permission denied - authentik' %} | {% trans 'Permission denied' %} - {{ config.authentik.branding.title }} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block card_title %} | {% block card_title %} | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								authentik/providers/ldap/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/providers/ldap/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										54
									
								
								authentik/providers/ldap/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								authentik/providers/ldap/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | """LDAPProvider 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.providers.ldap.models import LDAPProvider | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPProviderSerializer(ProviderSerializer): | ||||||
|  |     """LDAPProvider Serializer""" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         model = LDAPProvider | ||||||
|  |         fields = ProviderSerializer.Meta.fields + [ | ||||||
|  |             "base_dn", | ||||||
|  |             "search_group", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPProviderViewSet(ModelViewSet): | ||||||
|  |     """LDAPProvider Viewset""" | ||||||
|  |  | ||||||
|  |     queryset = LDAPProvider.objects.all() | ||||||
|  |     serializer_class = LDAPProviderSerializer | ||||||
|  |     ordering = ["name"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPOutpostConfigSerializer(ModelSerializer): | ||||||
|  |     """LDAPProvider Serializer""" | ||||||
|  |  | ||||||
|  |     application_slug = CharField(source="application.slug") | ||||||
|  |     bind_flow_slug = CharField(source="authorization_flow.slug") | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         model = LDAPProvider | ||||||
|  |         fields = [ | ||||||
|  |             "pk", | ||||||
|  |             "name", | ||||||
|  |             "base_dn", | ||||||
|  |             "bind_flow_slug", | ||||||
|  |             "application_slug", | ||||||
|  |             "search_group", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPOutpostConfigViewSet(ReadOnlyModelViewSet): | ||||||
|  |     """LDAPProvider Viewset""" | ||||||
|  |  | ||||||
|  |     queryset = LDAPProvider.objects.filter(application__isnull=False) | ||||||
|  |     serializer_class = LDAPOutpostConfigSerializer | ||||||
|  |     ordering = ["name"] | ||||||
							
								
								
									
										10
									
								
								authentik/providers/ldap/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								authentik/providers/ldap/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | """authentik ldap provider app config""" | ||||||
|  | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthentikProviderLDAPConfig(AppConfig): | ||||||
|  |     """authentik ldap provider app config""" | ||||||
|  |  | ||||||
|  |     name = "authentik.providers.ldap" | ||||||
|  |     label = "authentik_providers_ldap" | ||||||
|  |     verbose_name = "authentik Providers.LDAP" | ||||||
							
								
								
									
										0
									
								
								authentik/providers/ldap/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/providers/ldap/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										14
									
								
								authentik/providers/ldap/controllers/docker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/providers/ldap/controllers/docker.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | """LDAP Provider Docker Contoller""" | ||||||
|  | from authentik.outposts.controllers.base import DeploymentPort | ||||||
|  | from authentik.outposts.controllers.docker import DockerController | ||||||
|  | from authentik.outposts.models import DockerServiceConnection, Outpost | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPDockerController(DockerController): | ||||||
|  |     """LDAP Provider Docker Contoller""" | ||||||
|  |  | ||||||
|  |     def __init__(self, outpost: Outpost, connection: DockerServiceConnection): | ||||||
|  |         super().__init__(outpost, connection) | ||||||
|  |         self.deployment_ports = [ | ||||||
|  |             DeploymentPort(389, "ldap", "tcp", 3389), | ||||||
|  |         ] | ||||||
							
								
								
									
										14
									
								
								authentik/providers/ldap/controllers/kubernetes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/providers/ldap/controllers/kubernetes.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | """LDAP Provider Kubernetes Contoller""" | ||||||
|  | from authentik.outposts.controllers.base import DeploymentPort | ||||||
|  | from authentik.outposts.controllers.kubernetes import KubernetesController | ||||||
|  | from authentik.outposts.models import KubernetesServiceConnection, Outpost | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPKubernetesController(KubernetesController): | ||||||
|  |     """LDAP Provider Kubernetes Contoller""" | ||||||
|  |  | ||||||
|  |     def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection): | ||||||
|  |         super().__init__(outpost, connection) | ||||||
|  |         self.deployment_ports = [ | ||||||
|  |             DeploymentPort(389, "ldap", "tcp", 3389), | ||||||
|  |         ] | ||||||
							
								
								
									
										44
									
								
								authentik/providers/ldap/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								authentik/providers/ldap/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-04-26 12:45 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     initial = True | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0019_source_managed"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="LDAPProvider", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "provider_ptr", | ||||||
|  |                     models.OneToOneField( | ||||||
|  |                         auto_created=True, | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         parent_link=True, | ||||||
|  |                         primary_key=True, | ||||||
|  |                         serialize=False, | ||||||
|  |                         to="authentik_core.provider", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ( | ||||||
|  |                     "base_dn", | ||||||
|  |                     models.TextField( | ||||||
|  |                         default="DC=ldap,DC=goauthentik,DC=io", | ||||||
|  |                         help_text="DN under which objects are accessible.", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "LDAP Provider", | ||||||
|  |                 "verbose_name_plural": "LDAP Providers", | ||||||
|  |             }, | ||||||
|  |             bases=("authentik_core.provider", models.Model), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-04-26 19:57 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0019_source_managed"), | ||||||
|  |         ("authentik_providers_ldap", "0001_initial"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="ldapprovider", | ||||||
|  |             name="search_group", | ||||||
|  |             field=models.ForeignKey( | ||||||
|  |                 default=None, | ||||||
|  |                 help_text="Users in this group can do search queries. If not set, every user can execute search queries.", | ||||||
|  |                 null=True, | ||||||
|  |                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||||
|  |                 to="authentik_core.group", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										0
									
								
								authentik/providers/ldap/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/providers/ldap/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										55
									
								
								authentik/providers/ldap/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								authentik/providers/ldap/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | |||||||
|  | """LDAP Provider""" | ||||||
|  | from typing import Iterable, Optional, Type, Union | ||||||
|  |  | ||||||
|  | from django.db import models | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  | from rest_framework.serializers import Serializer | ||||||
|  |  | ||||||
|  | from authentik.core.models import Group, Provider | ||||||
|  | from authentik.outposts.models import OutpostModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LDAPProvider(OutpostModel, Provider): | ||||||
|  |     """Allow applications to authenticate against authentik's users using LDAP.""" | ||||||
|  |  | ||||||
|  |     base_dn = models.TextField( | ||||||
|  |         default="DC=ldap,DC=goauthentik,DC=io", | ||||||
|  |         help_text=_("DN under which objects are accessible."), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     search_group = models.ForeignKey( | ||||||
|  |         Group, | ||||||
|  |         null=True, | ||||||
|  |         default=None, | ||||||
|  |         on_delete=models.SET_DEFAULT, | ||||||
|  |         help_text=_( | ||||||
|  |             "Users in this group can do search queries. " | ||||||
|  |             "If not set, every user can execute search queries." | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def launch_url(self) -> Optional[str]: | ||||||
|  |         """LDAP never has a launch URL""" | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def component(self) -> str: | ||||||
|  |         return "ak-provider-ldap-form" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> Type[Serializer]: | ||||||
|  |         from authentik.providers.ldap.api import LDAPProviderSerializer | ||||||
|  |  | ||||||
|  |         return LDAPProviderSerializer | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return f"LDAP Provider {self.name}" | ||||||
|  |  | ||||||
|  |     def get_required_objects(self) -> Iterable[Union[models.Model, str]]: | ||||||
|  |         return [self, "authentik_core.view_user", "authentik_core.view_group"] | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         verbose_name = _("LDAP Provider") | ||||||
|  |         verbose_name_plural = _("LDAP Providers") | ||||||
| @ -38,6 +38,7 @@ class OAuth2ProviderSerializer(ProviderSerializer): | |||||||
|             "client_type", |             "client_type", | ||||||
|             "client_id", |             "client_id", | ||||||
|             "client_secret", |             "client_secret", | ||||||
|  |             "access_code_validity", | ||||||
|             "token_validity", |             "token_validity", | ||||||
|             "include_claims_in_id_token", |             "include_claims_in_id_token", | ||||||
|             "jwt_alg", |             "jwt_alg", | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """OAuth2Provider API Views""" | """OAuth2Provider API Views""" | ||||||
|  | from guardian.utils import get_anonymous_user | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
| from rest_framework.fields import CharField, ListField | from rest_framework.fields import CharField, ListField | ||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| @ -38,11 +39,10 @@ class AuthorizationCodeViewSet( | |||||||
|     ordering = ["provider", "expires"] |     ordering = ["provider", "expires"] | ||||||
|  |  | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         if not self.request: |         user = self.request.user if self.request else get_anonymous_user() | ||||||
|  |         if user.is_superuser: | ||||||
|             return super().get_queryset() |             return super().get_queryset() | ||||||
|         if self.request.user.is_superuser: |         return super().get_queryset().filter(user=user.pk) | ||||||
|             return super().get_queryset() |  | ||||||
|         return super().get_queryset().filter(user=self.request.user) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class RefreshTokenViewSet( | class RefreshTokenViewSet( | ||||||
| @ -59,8 +59,7 @@ class RefreshTokenViewSet( | |||||||
|     ordering = ["provider", "expires"] |     ordering = ["provider", "expires"] | ||||||
|  |  | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         if not self.request: |         user = self.request.user if self.request else get_anonymous_user() | ||||||
|  |         if user.is_superuser: | ||||||
|             return super().get_queryset() |             return super().get_queryset() | ||||||
|         if self.request.user.is_superuser: |         return super().get_queryset().filter(user=user.pk) | ||||||
|             return super().get_queryset() |  | ||||||
|         return super().get_queryset().filter(user=self.request.user) |  | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| """authentik auth oauth provider app config""" | """authentik oauth provider app config""" | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikProviderOAuth2Config(AppConfig): | class AuthentikProviderOAuth2Config(AppConfig): | ||||||
|     """authentik auth oauth provider app config""" |     """authentik oauth provider app config""" | ||||||
|  |  | ||||||
|     name = "authentik.providers.oauth2" |     name = "authentik.providers.oauth2" | ||||||
|     label = "authentik_providers_oauth2" |     label = "authentik_providers_oauth2" | ||||||
|  | |||||||
| @ -0,0 +1,24 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-04-28 18:17 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  | import authentik.lib.utils.time | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_providers_oauth2", "0011_managed"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="oauth2provider", | ||||||
|  |             name="access_code_validity", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 default="minutes=1", | ||||||
|  |                 help_text="Access codes not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3).", | ||||||
|  |                 validators=[authentik.lib.utils.time.timedelta_string_validator], | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -6,18 +6,18 @@ import time | |||||||
| from dataclasses import asdict, dataclass, field | from dataclasses import asdict, dataclass, field | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from hashlib import sha256 | from hashlib import sha256 | ||||||
| from typing import Any, Optional, Type | from typing import Any, Optional, Type, Union | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
|  | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey | ||||||
| from dacite import from_dict | from dacite import from_dict | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from django.utils import dateformat, timezone | from django.utils import dateformat, timezone | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key | from jwt import encode | ||||||
| from jwkest.jws import JWS |  | ||||||
| from rest_framework.serializers import Serializer | from rest_framework.serializers import Serializer | ||||||
|  |  | ||||||
| from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User | from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User | ||||||
| @ -175,6 +175,16 @@ class OAuth2Provider(Provider): | |||||||
|         ), |         ), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     access_code_validity = models.TextField( | ||||||
|  |         default="minutes=1", | ||||||
|  |         validators=[timedelta_string_validator], | ||||||
|  |         help_text=_( | ||||||
|  |             ( | ||||||
|  |                 "Access codes not valid on or after current time + this value " | ||||||
|  |                 "(Format: hours=1;minutes=2;seconds=3)." | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|     token_validity = models.TextField( |     token_validity = models.TextField( | ||||||
|         default="minutes=10", |         default="minutes=10", | ||||||
|         validators=[timedelta_string_validator], |         validators=[timedelta_string_validator], | ||||||
| @ -229,7 +239,7 @@ class OAuth2Provider(Provider): | |||||||
|         token.access_token = token.create_access_token(user, request) |         token.access_token = token.create_access_token(user, request) | ||||||
|         return token |         return token | ||||||
|  |  | ||||||
|     def get_jwt_keys(self) -> list[Key]: |     def get_jwt_keys(self) -> Union[RSAPrivateKey, str]: | ||||||
|         """ |         """ | ||||||
|         Takes a provider and returns the set of keys associated with it. |         Takes a provider and returns the set of keys associated with it. | ||||||
|         Returns a list of keys. |         Returns a list of keys. | ||||||
| @ -246,17 +256,10 @@ class OAuth2Provider(Provider): | |||||||
|                 self.jwt_alg = JWTAlgorithms.HS256 |                 self.jwt_alg = JWTAlgorithms.HS256 | ||||||
|                 self.save() |                 self.save() | ||||||
|             else: |             else: | ||||||
|                 # Because the JWT Library uses python cryptodome, |                 return self.rsa_key.private_key | ||||||
|                 # we can't directly pass the RSAPublicKey |  | ||||||
|                 # object, but have to load it ourselves |  | ||||||
|                 key = import_rsa_key(self.rsa_key.key_data) |  | ||||||
|                 keys = [RSAKey(key=key, kid=self.rsa_key.kid)] |  | ||||||
|                 if not keys: |  | ||||||
|                     raise Exception("You must add at least one RSA Key.") |  | ||||||
|                 return keys |  | ||||||
|  |  | ||||||
|         if self.jwt_alg == JWTAlgorithms.HS256: |         if self.jwt_alg == JWTAlgorithms.HS256: | ||||||
|             return [SYMKey(key=self.client_secret, alg=self.jwt_alg)] |             return self.client_secret | ||||||
|  |  | ||||||
|         raise Exception("Unsupported key algorithm.") |         raise Exception("Unsupported key algorithm.") | ||||||
|  |  | ||||||
| @ -297,11 +300,11 @@ class OAuth2Provider(Provider): | |||||||
|  |  | ||||||
|     def encode(self, payload: dict[str, Any]) -> str: |     def encode(self, payload: dict[str, Any]) -> str: | ||||||
|         """Represent the ID Token as a JSON Web Token (JWT).""" |         """Represent the ID Token as a JSON Web Token (JWT).""" | ||||||
|         keys = self.get_jwt_keys() |         key = self.get_jwt_keys() | ||||||
|         # If the provider does not have an RSA Key assigned, it was switched to Symmetric |         # If the provider does not have an RSA Key assigned, it was switched to Symmetric | ||||||
|         self.refresh_from_db() |         self.refresh_from_db() | ||||||
|         jws = JWS(payload, alg=self.jwt_alg) |         # pyright: reportGeneralTypeIssues=false | ||||||
|         return jws.sign_compact(keys) |         return encode(payload, key, algorithm=self.jwt_alg) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block title %} | {% block title %} | ||||||
| {% trans 'End session' %} | {% trans 'End session' %} - {{ config.authentik.branding.title }} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block card_title %} | {% block card_title %} | ||||||
|  | |||||||
							
								
								
									
										241
									
								
								authentik/providers/oauth2/tests/test_authorize.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								authentik/providers/oauth2/tests/test_authorize.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,241 @@ | |||||||
|  | """Test authorize view""" | ||||||
|  | from django.test import RequestFactory | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.encoding import force_str | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application, User | ||||||
|  | from authentik.flows.challenge import ChallengeTypes | ||||||
|  | from authentik.flows.models import Flow | ||||||
|  | from authentik.providers.oauth2.errors import ( | ||||||
|  |     AuthorizeError, | ||||||
|  |     ClientIdError, | ||||||
|  |     RedirectUriError, | ||||||
|  | ) | ||||||
|  | from authentik.providers.oauth2.generators import ( | ||||||
|  |     generate_client_id, | ||||||
|  |     generate_client_secret, | ||||||
|  | ) | ||||||
|  | from authentik.providers.oauth2.models import ( | ||||||
|  |     AuthorizationCode, | ||||||
|  |     GrantTypes, | ||||||
|  |     OAuth2Provider, | ||||||
|  |     RefreshToken, | ||||||
|  | ) | ||||||
|  | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  | from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestAuthorize(OAuthTestCase): | ||||||
|  |     """Test authorize view""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         super().setUp() | ||||||
|  |         self.factory = RequestFactory() | ||||||
|  |  | ||||||
|  |     def test_invalid_grant_type(self): | ||||||
|  |         """Test with invalid grant type""" | ||||||
|  |         with self.assertRaises(AuthorizeError): | ||||||
|  |             request = self.factory.get("/", data={"response_type": "invalid"}) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|  |     def test_invalid_client_id(self): | ||||||
|  |         """Test invalid client ID""" | ||||||
|  |         with self.assertRaises(ClientIdError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", data={"response_type": "code", "client_id": "invalid"} | ||||||
|  |             ) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|  |     def test_request(self): | ||||||
|  |         """test request param""" | ||||||
|  |         OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         with self.assertRaises(AuthorizeError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", | ||||||
|  |                 data={ | ||||||
|  |                     "response_type": "code", | ||||||
|  |                     "client_id": "test", | ||||||
|  |                     "redirect_uri": "http://local.invalid", | ||||||
|  |                     "request": "foo", | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|  |     def test_redirect_uri(self): | ||||||
|  |         """test missing/invalid redirect URI""" | ||||||
|  |         OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         with self.assertRaises(RedirectUriError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", data={"response_type": "code", "client_id": "test"} | ||||||
|  |             ) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |         with self.assertRaises(RedirectUriError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", | ||||||
|  |                 data={ | ||||||
|  |                     "response_type": "code", | ||||||
|  |                     "client_id": "test", | ||||||
|  |                     "redirect_uri": "http://localhost", | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|  |     def test_response_type(self): | ||||||
|  |         """test response_type""" | ||||||
|  |         OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         request = self.factory.get( | ||||||
|  |             "/", | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "code", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             OAuthAuthorizationParams.from_request(request).grant_type, | ||||||
|  |             GrantTypes.AUTHORIZATION_CODE, | ||||||
|  |         ) | ||||||
|  |         request = self.factory.get( | ||||||
|  |             "/", | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "id_token", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |                 "scope": "openid", | ||||||
|  |                 "state": "foo", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             OAuthAuthorizationParams.from_request(request).grant_type, | ||||||
|  |             GrantTypes.IMPLICIT, | ||||||
|  |         ) | ||||||
|  |         # Implicit without openid scope | ||||||
|  |         with self.assertRaises(AuthorizeError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", | ||||||
|  |                 data={ | ||||||
|  |                     "response_type": "id_token", | ||||||
|  |                     "client_id": "test", | ||||||
|  |                     "redirect_uri": "http://local.invalid", | ||||||
|  |                     "state": "foo", | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             self.assertEqual( | ||||||
|  |                 OAuthAuthorizationParams.from_request(request).grant_type, | ||||||
|  |                 GrantTypes.IMPLICIT, | ||||||
|  |             ) | ||||||
|  |         request = self.factory.get( | ||||||
|  |             "/", | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "code token", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |                 "scope": "openid", | ||||||
|  |                 "state": "foo", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             OAuthAuthorizationParams.from_request(request).grant_type, GrantTypes.HYBRID | ||||||
|  |         ) | ||||||
|  |         with self.assertRaises(AuthorizeError): | ||||||
|  |             request = self.factory.get( | ||||||
|  |                 "/", | ||||||
|  |                 data={ | ||||||
|  |                     "response_type": "invalid", | ||||||
|  |                     "client_id": "test", | ||||||
|  |                     "redirect_uri": "http://local.invalid", | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             OAuthAuthorizationParams.from_request(request) | ||||||
|  |  | ||||||
|  |     def test_full_code(self): | ||||||
|  |         """Test full authorization""" | ||||||
|  |         flow = Flow.objects.create(slug="empty") | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id="test", | ||||||
|  |             authorization_flow=flow, | ||||||
|  |             redirect_uris="foo://localhost", | ||||||
|  |         ) | ||||||
|  |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  |         state = generate_client_id() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         self.client.force_login(user) | ||||||
|  |         # Step 1, initiate params and get redirect to flow | ||||||
|  |         self.client.get( | ||||||
|  |             reverse("authentik_providers_oauth2:authorize"), | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "code", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "state": state, | ||||||
|  |                 "redirect_uri": "foo://localhost", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |         ) | ||||||
|  |         code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                 "to": f"foo://localhost?code={code.code}&state={state}", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_full_implicit(self): | ||||||
|  |         """Test full authorization""" | ||||||
|  |         flow = Flow.objects.create(slug="empty") | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id="test", | ||||||
|  |             client_secret=generate_client_secret(), | ||||||
|  |             authorization_flow=flow, | ||||||
|  |             redirect_uris="http://localhost", | ||||||
|  |         ) | ||||||
|  |         Application.objects.create(name="app", slug="app", provider=provider) | ||||||
|  |         state = generate_client_id() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         self.client.force_login(user) | ||||||
|  |         # Step 1, initiate params and get redirect to flow | ||||||
|  |         self.client.get( | ||||||
|  |             reverse("authentik_providers_oauth2:authorize"), | ||||||
|  |             data={ | ||||||
|  |                 "response_type": "id_token", | ||||||
|  |                 "client_id": "test", | ||||||
|  |                 "state": state, | ||||||
|  |                 "scope": "openid", | ||||||
|  |                 "redirect_uri": "http://localhost", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||||
|  |         ) | ||||||
|  |         token: RefreshToken = RefreshToken.objects.filter(user=user).first() | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                 "to": ( | ||||||
|  |                     f"http://localhost#access_token={token.access_token}" | ||||||
|  |                     f"&id_token={provider.encode(token.id_token.to_dict())}&token_type=bearer" | ||||||
|  |                     f"&expires_in=600&state={state}" | ||||||
|  |                 ), | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.validate_jwt(token, provider) | ||||||
							
								
								
									
										232
									
								
								authentik/providers/oauth2/tests/test_token.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								authentik/providers/oauth2/tests/test_token.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,232 @@ | |||||||
|  | """Test token view""" | ||||||
|  | from base64 import b64encode | ||||||
|  |  | ||||||
|  | from django.test import RequestFactory | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.encoding import force_str | ||||||
|  |  | ||||||
|  | from authentik.core.models import Application, User | ||||||
|  | from authentik.flows.models import Flow | ||||||
|  | from authentik.providers.oauth2.constants import ( | ||||||
|  |     GRANT_TYPE_AUTHORIZATION_CODE, | ||||||
|  |     GRANT_TYPE_REFRESH_TOKEN, | ||||||
|  | ) | ||||||
|  | from authentik.providers.oauth2.generators import ( | ||||||
|  |     generate_client_id, | ||||||
|  |     generate_client_secret, | ||||||
|  | ) | ||||||
|  | from authentik.providers.oauth2.models import ( | ||||||
|  |     AuthorizationCode, | ||||||
|  |     OAuth2Provider, | ||||||
|  |     RefreshToken, | ||||||
|  | ) | ||||||
|  | from authentik.providers.oauth2.tests.utils import OAuthTestCase | ||||||
|  | from authentik.providers.oauth2.views.token import TokenParams | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestToken(OAuthTestCase): | ||||||
|  |     """Test token view""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         super().setUp() | ||||||
|  |         self.factory = RequestFactory() | ||||||
|  |         self.app = Application.objects.create(name="test", slug="test") | ||||||
|  |  | ||||||
|  |     def test_request_auth_code(self): | ||||||
|  |         """test request param""" | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id=generate_client_id(), | ||||||
|  |             client_secret=generate_client_secret(), | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         header = b64encode( | ||||||
|  |             f"{provider.client_id}:{provider.client_secret}".encode() | ||||||
|  |         ).decode() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         code = AuthorizationCode.objects.create( | ||||||
|  |             code="foobar", provider=provider, user=user | ||||||
|  |         ) | ||||||
|  |         request = self.factory.post( | ||||||
|  |             "/", | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, | ||||||
|  |                 "code": code.code, | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |         ) | ||||||
|  |         params = TokenParams.from_request(request) | ||||||
|  |         self.assertEqual(params.provider, provider) | ||||||
|  |  | ||||||
|  |     def test_request_refresh_token(self): | ||||||
|  |         """test request param""" | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id=generate_client_id(), | ||||||
|  |             client_secret=generate_client_secret(), | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         header = b64encode( | ||||||
|  |             f"{provider.client_id}:{provider.client_secret}".encode() | ||||||
|  |         ).decode() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         token: RefreshToken = RefreshToken.objects.create( | ||||||
|  |             provider=provider, | ||||||
|  |             user=user, | ||||||
|  |             refresh_token=generate_client_id(), | ||||||
|  |         ) | ||||||
|  |         request = self.factory.post( | ||||||
|  |             "/", | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_REFRESH_TOKEN, | ||||||
|  |                 "refresh_token": token.refresh_token, | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |         ) | ||||||
|  |         params = TokenParams.from_request(request) | ||||||
|  |         self.assertEqual(params.provider, provider) | ||||||
|  |  | ||||||
|  |     def test_auth_code_view(self): | ||||||
|  |         """test request param""" | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id=generate_client_id(), | ||||||
|  |             client_secret=generate_client_secret(), | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         # Needs to be assigned to an application for iss to be set | ||||||
|  |         self.app.provider = provider | ||||||
|  |         self.app.save() | ||||||
|  |         header = b64encode( | ||||||
|  |             f"{provider.client_id}:{provider.client_secret}".encode() | ||||||
|  |         ).decode() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         code = AuthorizationCode.objects.create( | ||||||
|  |             code="foobar", provider=provider, user=user, is_open_id=True | ||||||
|  |         ) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_providers_oauth2:token"), | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, | ||||||
|  |                 "code": code.code, | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |         ) | ||||||
|  |         new_token: RefreshToken = RefreshToken.objects.filter(user=user).first() | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "access_token": new_token.access_token, | ||||||
|  |                 "refresh_token": new_token.refresh_token, | ||||||
|  |                 "token_type": "bearer", | ||||||
|  |                 "expires_in": 600, | ||||||
|  |                 "id_token": provider.encode( | ||||||
|  |                     new_token.id_token.to_dict(), | ||||||
|  |                 ), | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.validate_jwt(new_token, provider) | ||||||
|  |  | ||||||
|  |     def test_refresh_token_view(self): | ||||||
|  |         """test request param""" | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id=generate_client_id(), | ||||||
|  |             client_secret=generate_client_secret(), | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         # Needs to be assigned to an application for iss to be set | ||||||
|  |         self.app.provider = provider | ||||||
|  |         self.app.save() | ||||||
|  |         header = b64encode( | ||||||
|  |             f"{provider.client_id}:{provider.client_secret}".encode() | ||||||
|  |         ).decode() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         token: RefreshToken = RefreshToken.objects.create( | ||||||
|  |             provider=provider, | ||||||
|  |             user=user, | ||||||
|  |             refresh_token=generate_client_id(), | ||||||
|  |         ) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_providers_oauth2:token"), | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_REFRESH_TOKEN, | ||||||
|  |                 "refresh_token": token.refresh_token, | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |             HTTP_ORIGIN="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         new_token: RefreshToken = ( | ||||||
|  |             RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first() | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response["Access-Control-Allow-Credentials"], "true") | ||||||
|  |         self.assertEqual( | ||||||
|  |             response["Access-Control-Allow-Origin"], "http://local.invalid" | ||||||
|  |         ) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "access_token": new_token.access_token, | ||||||
|  |                 "refresh_token": new_token.refresh_token, | ||||||
|  |                 "token_type": "bearer", | ||||||
|  |                 "expires_in": 600, | ||||||
|  |                 "id_token": provider.encode( | ||||||
|  |                     new_token.id_token.to_dict(), | ||||||
|  |                 ), | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         self.validate_jwt(new_token, provider) | ||||||
|  |  | ||||||
|  |     def test_refresh_token_view_invalid_origin(self): | ||||||
|  |         """test request param""" | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             client_id=generate_client_id(), | ||||||
|  |             client_secret=generate_client_secret(), | ||||||
|  |             authorization_flow=Flow.objects.first(), | ||||||
|  |             redirect_uris="http://local.invalid", | ||||||
|  |         ) | ||||||
|  |         header = b64encode( | ||||||
|  |             f"{provider.client_id}:{provider.client_secret}".encode() | ||||||
|  |         ).decode() | ||||||
|  |         user = User.objects.get(username="akadmin") | ||||||
|  |         token: RefreshToken = RefreshToken.objects.create( | ||||||
|  |             provider=provider, | ||||||
|  |             user=user, | ||||||
|  |             refresh_token=generate_client_id(), | ||||||
|  |         ) | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_providers_oauth2:token"), | ||||||
|  |             data={ | ||||||
|  |                 "grant_type": GRANT_TYPE_REFRESH_TOKEN, | ||||||
|  |                 "refresh_token": token.refresh_token, | ||||||
|  |                 "redirect_uri": "http://local.invalid", | ||||||
|  |             }, | ||||||
|  |             HTTP_AUTHORIZATION=f"Basic {header}", | ||||||
|  |             HTTP_ORIGIN="http://another.invalid", | ||||||
|  |         ) | ||||||
|  |         new_token: RefreshToken = ( | ||||||
|  |             RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first() | ||||||
|  |         ) | ||||||
|  |         self.assertNotIn("Access-Control-Allow-Credentials", response) | ||||||
|  |         self.assertNotIn("Access-Control-Allow-Origin", response) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "access_token": new_token.access_token, | ||||||
|  |                 "refresh_token": new_token.refresh_token, | ||||||
|  |                 "token_type": "bearer", | ||||||
|  |                 "expires_in": 600, | ||||||
|  |                 "id_token": provider.encode( | ||||||
|  |                     new_token.id_token.to_dict(), | ||||||
|  |                 ), | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
| @ -1,46 +0,0 @@ | |||||||
| """Test authorize view""" |  | ||||||
| from django.test import RequestFactory, TestCase |  | ||||||
|  |  | ||||||
| from authentik.flows.models import Flow |  | ||||||
| from authentik.providers.oauth2.errors import ( |  | ||||||
|     AuthorizeError, |  | ||||||
|     ClientIdError, |  | ||||||
|     RedirectUriError, |  | ||||||
| ) |  | ||||||
| from authentik.providers.oauth2.models import OAuth2Provider |  | ||||||
| from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestViewsAuthorize(TestCase): |  | ||||||
|     """Test authorize view""" |  | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |  | ||||||
|         super().setUp() |  | ||||||
|         self.factory = RequestFactory() |  | ||||||
|  |  | ||||||
|     def test_invalid_grant_type(self): |  | ||||||
|         """Test with invalid grant type""" |  | ||||||
|         with self.assertRaises(AuthorizeError): |  | ||||||
|             request = self.factory.get("/", data={"response_type": "invalid"}) |  | ||||||
|             OAuthAuthorizationParams.from_request(request) |  | ||||||
|  |  | ||||||
|     def test_invalid_client_id(self): |  | ||||||
|         """Test invalid client ID""" |  | ||||||
|         with self.assertRaises(ClientIdError): |  | ||||||
|             request = self.factory.get( |  | ||||||
|                 "/", data={"response_type": "code", "client_id": "invalid"} |  | ||||||
|             ) |  | ||||||
|             OAuthAuthorizationParams.from_request(request) |  | ||||||
|  |  | ||||||
|     def test_missing_redirect_uri(self): |  | ||||||
|         """test missing redirect URI""" |  | ||||||
|         OAuth2Provider.objects.create( |  | ||||||
|             name="test", |  | ||||||
|             client_id="test", |  | ||||||
|             authorization_flow=Flow.objects.first(), |  | ||||||
|         ) |  | ||||||
|         with self.assertRaises(RedirectUriError): |  | ||||||
|             request = self.factory.get( |  | ||||||
|                 "/", data={"response_type": "code", "client_id": "test"} |  | ||||||
|             ) |  | ||||||
|             OAuthAuthorizationParams.from_request(request) |  | ||||||
							
								
								
									
										31
									
								
								authentik/providers/oauth2/tests/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								authentik/providers/oauth2/tests/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | """OAuth test helpers""" | ||||||
|  | from django.test import TestCase | ||||||
|  | from jwt import decode | ||||||
|  |  | ||||||
|  | from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OAuthTestCase(TestCase): | ||||||
|  |     """OAuth test helpers""" | ||||||
|  |  | ||||||
|  |     required_jwt_keys = [ | ||||||
|  |         "exp", | ||||||
|  |         "iat", | ||||||
|  |         "auth_time", | ||||||
|  |         "acr", | ||||||
|  |         "sub", | ||||||
|  |         "iss", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def validate_jwt(self, token: RefreshToken, provider: OAuth2Provider): | ||||||
|  |         """Validate that all required fields are set""" | ||||||
|  |         jwt = decode( | ||||||
|  |             token.access_token, | ||||||
|  |             provider.client_secret, | ||||||
|  |             algorithms=[provider.jwt_alg], | ||||||
|  |             audience=provider.client_id, | ||||||
|  |         ) | ||||||
|  |         id_token = token.id_token.to_dict() | ||||||
|  |         for key in self.required_jwt_keys: | ||||||
|  |             self.assertIsNotNone(jwt[key], f"Key {key} is missing in access_token") | ||||||
|  |             self.assertIsNotNone(id_token[key], f"Key {key} is missing in id_token") | ||||||
| @ -2,9 +2,11 @@ | |||||||
| import re | import re | ||||||
| from base64 import b64decode | from base64 import b64decode | ||||||
| from binascii import Error | from binascii import Error | ||||||
| from typing import Optional | from typing import Any, Optional | ||||||
|  | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | from django.http import HttpRequest, HttpResponse, JsonResponse | ||||||
|  | from django.http.response import HttpResponseRedirect | ||||||
| from django.utils.cache import patch_vary_headers | from django.utils.cache import patch_vary_headers | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| @ -25,15 +27,34 @@ class TokenResponse(JsonResponse): | |||||||
|         self["Pragma"] = "no-cache" |         self["Pragma"] = "no-cache" | ||||||
|  |  | ||||||
|  |  | ||||||
| def cors_allow_any(request, response): | def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: str): | ||||||
|     """ |     """Add headers to permit CORS requests from allowed_origins, with or without credentials, | ||||||
|     Add headers to permit CORS requests from any origin, with or without credentials, |     with any headers.""" | ||||||
|     with any headers. |  | ||||||
|     """ |  | ||||||
|     origin = request.META.get("HTTP_ORIGIN") |     origin = request.META.get("HTTP_ORIGIN") | ||||||
|     if not origin: |     if not origin: | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|  |     # OPTIONS requests don't have an authorization header -> hence | ||||||
|  |     # we can't extract the provider this request is for | ||||||
|  |     # so for options requests we allow the calling origin without checking | ||||||
|  |     allowed = request.method == "OPTIONS" | ||||||
|  |     received_origin = urlparse(origin) | ||||||
|  |     for allowed_origin in allowed_origins: | ||||||
|  |         url = urlparse(allowed_origin) | ||||||
|  |         if ( | ||||||
|  |             received_origin.scheme == url.scheme | ||||||
|  |             and received_origin.hostname == url.hostname | ||||||
|  |             and received_origin.port == url.port | ||||||
|  |         ): | ||||||
|  |             allowed = True | ||||||
|  |     if not allowed: | ||||||
|  |         LOGGER.warning( | ||||||
|  |             "CORS: Origin is not an allowed origin", | ||||||
|  |             requested=origin, | ||||||
|  |             allowed=allowed_origins, | ||||||
|  |         ) | ||||||
|  |         return response | ||||||
|  |  | ||||||
|     # From the CORS spec: The string "*" cannot be used for a resource that supports credentials. |     # From the CORS spec: The string "*" cannot be used for a resource that supports credentials. | ||||||
|     response["Access-Control-Allow-Origin"] = origin |     response["Access-Control-Allow-Origin"] = origin | ||||||
|     patch_vary_headers(response, ["Origin"]) |     patch_vary_headers(response, ["Origin"]) | ||||||
| @ -141,3 +162,18 @@ def protected_resource_view(scopes: list[str]): | |||||||
|         return view_wrapper |         return view_wrapper | ||||||
|  |  | ||||||
|     return wrapper |     return wrapper | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HttpResponseRedirectScheme(HttpResponseRedirect): | ||||||
|  |     """HTTP Response to redirect, can be to a non-http scheme""" | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         redirect_to: str, | ||||||
|  |         *args: Any, | ||||||
|  |         allowed_schemes: Optional[list[str]] = None, | ||||||
|  |         **kwargs: Any, | ||||||
|  |     ) -> None: | ||||||
|  |         self.allowed_schemes = allowed_schemes or ["http", "https", "ftp"] | ||||||
|  |         # pyright: reportGeneralTypeIssues=false | ||||||
|  |         super().__init__(redirect_to, *args, **kwargs) | ||||||
|  | |||||||
| @ -2,12 +2,12 @@ | |||||||
| from dataclasses import dataclass, field | from dataclasses import dataclass, field | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| from typing import Optional | from typing import Optional | ||||||
| from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit | from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.http.response import Http404, HttpResponseRedirect | from django.http.response import Http404, HttpResponseBadRequest, HttpResponseRedirect | ||||||
| from django.shortcuts import get_object_or_404, redirect | from django.shortcuts import get_object_or_404 | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| @ -46,6 +46,7 @@ from authentik.providers.oauth2.models import ( | |||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|     ResponseTypes, |     ResponseTypes, | ||||||
| ) | ) | ||||||
|  | from authentik.providers.oauth2.utils import HttpResponseRedirectScheme | ||||||
| from authentik.providers.oauth2.views.userinfo import UserInfoView | from authentik.providers.oauth2.views.userinfo import UserInfoView | ||||||
| from authentik.stages.consent.models import ConsentMode, ConsentStage | from authentik.stages.consent.models import ConsentMode, ConsentStage | ||||||
| from authentik.stages.consent.stage import ( | from authentik.stages.consent.stage import ( | ||||||
| @ -218,7 +219,7 @@ class OAuthAuthorizationParams: | |||||||
|             code.code_challenge_method = self.code_challenge_method |             code.code_challenge_method = self.code_challenge_method | ||||||
|  |  | ||||||
|         code.expires_at = timezone.now() + timedelta_from_string( |         code.expires_at = timezone.now() + timedelta_from_string( | ||||||
|             self.provider.token_validity |             self.provider.access_code_validity | ||||||
|         ) |         ) | ||||||
|         code.scope = self.scope |         code.scope = self.scope | ||||||
|         code.nonce = self.nonce |         code.nonce = self.nonce | ||||||
| @ -233,9 +234,17 @@ class OAuthFulfillmentStage(StageView): | |||||||
|     params: OAuthAuthorizationParams |     params: OAuthAuthorizationParams | ||||||
|     provider: OAuth2Provider |     provider: OAuth2Provider | ||||||
|  |  | ||||||
|  |     def redirect(self, uri: str) -> HttpResponse: | ||||||
|  |         """Redirect using HttpResponseRedirectScheme, compatible with non-http schemes""" | ||||||
|  |         parsed = urlparse(uri) | ||||||
|  |         return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme]) | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument |     # pylint: disable=unused-argument | ||||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: |     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|         """final Stage of an OAuth2 Flow""" |         """final Stage of an OAuth2 Flow""" | ||||||
|  |         if PLAN_CONTEXT_PARAMS not in self.executor.plan.context: | ||||||
|  |             LOGGER.warning("Got to fulfillment stage with no pending context") | ||||||
|  |             return HttpResponseBadRequest() | ||||||
|         self.params: OAuthAuthorizationParams = self.executor.plan.context.pop( |         self.params: OAuthAuthorizationParams = self.executor.plan.context.pop( | ||||||
|             PLAN_CONTEXT_PARAMS |             PLAN_CONTEXT_PARAMS | ||||||
|         ) |         ) | ||||||
| @ -258,7 +267,7 @@ class OAuthFulfillmentStage(StageView): | |||||||
|                 flow=self.executor.plan.flow_pk, |                 flow=self.executor.plan.flow_pk, | ||||||
|                 scopes=", ".join(self.params.scope), |                 scopes=", ".join(self.params.scope), | ||||||
|             ).from_http(self.request) |             ).from_http(self.request) | ||||||
|             return redirect(self.create_response_uri()) |             return self.redirect(self.create_response_uri()) | ||||||
|         except (ClientIdError, RedirectUriError) as error: |         except (ClientIdError, RedirectUriError) as error: | ||||||
|             error.to_event(application=application).from_http(request) |             error.to_event(application=application).from_http(request) | ||||||
|             self.executor.stage_invalid() |             self.executor.stage_invalid() | ||||||
| @ -267,7 +276,7 @@ class OAuthFulfillmentStage(StageView): | |||||||
|         except AuthorizeError as error: |         except AuthorizeError as error: | ||||||
|             error.to_event(application=application).from_http(request) |             error.to_event(application=application).from_http(request) | ||||||
|             self.executor.stage_invalid() |             self.executor.stage_invalid() | ||||||
|             return redirect(error.create_uri()) |             return self.redirect(error.create_uri()) | ||||||
|  |  | ||||||
|     def create_response_uri(self) -> str: |     def create_response_uri(self) -> str: | ||||||
|         """Create a final Response URI the user is redirected to.""" |         """Create a final Response URI the user is redirected to.""" | ||||||
| @ -282,7 +291,7 @@ class OAuthFulfillmentStage(StageView): | |||||||
|                 GrantTypes.HYBRID, |                 GrantTypes.HYBRID, | ||||||
|             ]: |             ]: | ||||||
|                 code = self.params.create_code(self.request) |                 code = self.params.create_code(self.request) | ||||||
|                 code.save() |                 code.save(force_insert=True) | ||||||
|  |  | ||||||
|             if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE: |             if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE: | ||||||
|                 query_params["code"] = code.code |                 query_params["code"] = code.code | ||||||
| @ -301,7 +310,7 @@ class OAuthFulfillmentStage(StageView): | |||||||
|                 return urlunsplit(uri) |                 return urlunsplit(uri) | ||||||
|             raise OAuth2Error() |             raise OAuth2Error() | ||||||
|         except OAuth2Error as error: |         except OAuth2Error as error: | ||||||
|             LOGGER.exception("Error when trying to create response uri", error=error) |             LOGGER.warning("Error when trying to create response uri", error=error) | ||||||
|             raise AuthorizeError( |             raise AuthorizeError( | ||||||
|                 self.params.redirect_uri, |                 self.params.redirect_uri, | ||||||
|                 "server_error", |                 "server_error", | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	