Compare commits
	
		
			133 Commits
		
	
	
		
			version-0.
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6f1fb9ca43 | |||
| 09f56f1f01 | |||
| 3d3a0cd9e3 | |||
| 32667f37d1 | |||
| 9532c4df9d | |||
| fd90979832 | |||
| 2e20d5dfbf | |||
| 33f06f0799 | |||
| 920736fc77 | |||
| ee8e42728e | |||
| 204792b750 | |||
| 8ffa3e5885 | |||
| 175d3b3377 | |||
| d5f35798dc | |||
| 1a0aa7e944 | |||
| 677a181b9c | |||
| 4b551add1a | |||
| 90220e911f | |||
| 217cca822d | |||
| e6f897c7e6 | |||
| 65c9d4bf4c | |||
| 6e88e52d78 | |||
| 4e884e80ab | |||
| d19bfebce3 | |||
| b86d4a455d | |||
| 222cece3e1 | |||
| 6e69edf1af | |||
| 55aab5660b | |||
| 08e7ef3c1e | |||
| d728163eea | |||
| cbf246694c | |||
| 9d0a01012d | |||
| cf76652a4c | |||
| c525ecc334 | |||
| 49d40d4337 | |||
| 94182f88a4 | |||
| 1c25f4f09b | |||
| 6495d6c50a | |||
| b81f3e4a38 | |||
| aad3b43ac3 | |||
| 60f52f102a | |||
| f3ccb5341d | |||
| cb73210447 | |||
| 81efc9a673 | |||
| 72c6c0da9b | |||
| 8fef839965 | |||
| 87b830ff9a | |||
| 8acb9dde5f | |||
| 36e8b1004c | |||
| f959212692 | |||
| 2d2a404028 | |||
| 394ad6ade5 | |||
| 4baf9e4a22 | |||
| d020599e09 | |||
| 4f28a89e63 | |||
| f8b4b92e8d | |||
| 33f208657c | |||
| c1fbfc63ab | |||
| 192dbe05c4 | |||
| 0b41cb84f0 | |||
| d637bd0bf9 | |||
| a2bddc6d91 | |||
| 2e42da11ea | |||
| f297d1256d | |||
| 5e1e5afb24 | |||
| da59e7c4a7 | |||
| 8684d106d5 | |||
| 2579e168c3 | |||
| 7f5caf901d | |||
| 1c686e19b5 | |||
| 3cc92f6c97 | |||
| 8f5b33a3a2 | |||
| 4447345345 | |||
| 42c6401ba7 | |||
| eef111bcfd | |||
| 6192b2787f | |||
| c7d28f8ca9 | |||
| 1342266368 | |||
| 7ff679b1a3 | |||
| 8beddcddb0 | |||
| 9fe8554f28 | |||
| 812fe72e60 | |||
| d0e4533cdd | |||
| b1b5d94ddc | |||
| 59722e0bbe | |||
| 9c5bb3998c | |||
| c180c4b1a2 | |||
| 308896719d | |||
| 95c1473dd2 | |||
| b14c5039ed | |||
| b6948334f2 | |||
| 29e08e7477 | |||
| 36bc1dc020 | |||
| 61d1407804 | |||
| 47ddf0d7f2 | |||
| cb36a3c8c7 | |||
| cac94792fa | |||
| 6f56c37d2f | |||
| 8369fa16ae | |||
| f30bdbecd6 | |||
| c727c845df | |||
| b2b737e59e | |||
| e2b930afe3 | |||
| 36c0b924bc | |||
| 1ccf6dcf6f | |||
| f8a426f0e8 | |||
| f8756d0fc9 | |||
| fd6d99f4f9 | |||
| 04379f2c90 | |||
| ba1195cf70 | |||
| b0bd9212c7 | |||
| 209179e012 | |||
| df16f635fa | |||
| 14ccf47a2b | |||
| 2aac024477 | |||
| 4743e72e18 | |||
| cab2942c4e | |||
| 9fb5ce2a1a | |||
| 0eab4489c5 | |||
| 3aae030b23 | |||
| e7060cb90a | |||
| 6c0b9e3525 | |||
| 82bb179bc2 | |||
| 774eb0388b | |||
| 6ed78830a0 | |||
| 6fe323f1a7 | |||
| 85c2db018e | |||
| bc9e7e8b93 | |||
| 08c58ce3fb | |||
| c3bc986473 | |||
| 2e69efe699 | |||
| 4daa373dcf | |||
| a85b8a65c0 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 0.14.2-stable
 | 
					current_version = 2021.1.2-stable
 | 
				
			||||||
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>.*)
 | 
				
			||||||
@ -31,6 +31,6 @@ values =
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:authentik/__init__.py]
 | 
					[bumpversion:file:authentik/__init__.py]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:proxy/pkg/version.go]
 | 
					[bumpversion:file:outpost/pkg/version.go]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:web/src/constants.ts]
 | 
					[bumpversion:file:web/src/constants.ts]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -18,11 +18,11 @@ jobs:
 | 
				
			|||||||
      - name: Building Docker Image
 | 
					      - name: Building Docker Image
 | 
				
			||||||
        run: docker build
 | 
					        run: docker build
 | 
				
			||||||
          --no-cache
 | 
					          --no-cache
 | 
				
			||||||
          -t beryju/authentik:0.14.2-stable
 | 
					          -t beryju/authentik:2021.1.2-stable
 | 
				
			||||||
          -t beryju/authentik:latest
 | 
					          -t beryju/authentik:latest
 | 
				
			||||||
          -f Dockerfile .
 | 
					          -f Dockerfile .
 | 
				
			||||||
      - name: Push Docker Container to Registry (versioned)
 | 
					      - name: Push Docker Container to Registry (versioned)
 | 
				
			||||||
        run: docker push beryju/authentik:0.14.2-stable
 | 
					        run: docker push beryju/authentik:2021.1.2-stable
 | 
				
			||||||
      - name: Push Docker Container to Registry (latest)
 | 
					      - name: Push Docker Container to Registry (latest)
 | 
				
			||||||
        run: docker push beryju/authentik:latest
 | 
					        run: docker push beryju/authentik:latest
 | 
				
			||||||
  build-proxy:
 | 
					  build-proxy:
 | 
				
			||||||
@ -34,7 +34,7 @@ jobs:
 | 
				
			|||||||
          go-version: "^1.15"
 | 
					          go-version: "^1.15"
 | 
				
			||||||
      - name: prepare go api client
 | 
					      - name: prepare go api client
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          cd proxy
 | 
					          cd outpost
 | 
				
			||||||
          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 .
 | 
				
			||||||
@ -45,14 +45,14 @@ jobs:
 | 
				
			|||||||
        run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
 | 
					        run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
 | 
				
			||||||
      - name: Building Docker Image
 | 
					      - name: Building Docker Image
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          cd proxy/
 | 
					          cd outpost/
 | 
				
			||||||
          docker build \
 | 
					          docker build \
 | 
				
			||||||
          --no-cache \
 | 
					          --no-cache \
 | 
				
			||||||
          -t beryju/authentik-proxy:0.14.2-stable \
 | 
					          -t beryju/authentik-proxy:2021.1.2-stable \
 | 
				
			||||||
          -t beryju/authentik-proxy:latest \
 | 
					          -t beryju/authentik-proxy:latest \
 | 
				
			||||||
          -f Dockerfile .
 | 
					          -f proxy.Dockerfile .
 | 
				
			||||||
      - name: Push Docker Container to Registry (versioned)
 | 
					      - name: Push Docker Container to Registry (versioned)
 | 
				
			||||||
        run: docker push beryju/authentik-proxy:0.14.2-stable
 | 
					        run: docker push beryju/authentik-proxy:2021.1.2-stable
 | 
				
			||||||
      - name: Push Docker Container to Registry (latest)
 | 
					      - name: Push Docker Container to Registry (latest)
 | 
				
			||||||
        run: docker push beryju/authentik-proxy:latest
 | 
					        run: docker push beryju/authentik-proxy:latest
 | 
				
			||||||
  build-static:
 | 
					  build-static:
 | 
				
			||||||
@ -69,11 +69,11 @@ jobs:
 | 
				
			|||||||
          cd web/
 | 
					          cd web/
 | 
				
			||||||
          docker build \
 | 
					          docker build \
 | 
				
			||||||
          --no-cache \
 | 
					          --no-cache \
 | 
				
			||||||
          -t beryju/authentik-static:0.14.2-stable \
 | 
					          -t beryju/authentik-static:2021.1.2-stable \
 | 
				
			||||||
          -t beryju/authentik-static:latest \
 | 
					          -t beryju/authentik-static:latest \
 | 
				
			||||||
          -f Dockerfile .
 | 
					          -f Dockerfile .
 | 
				
			||||||
      - name: Push Docker Container to Registry (versioned)
 | 
					      - name: Push Docker Container to Registry (versioned)
 | 
				
			||||||
        run: docker push beryju/authentik-static:0.14.2-stable
 | 
					        run: docker push beryju/authentik-static:2021.1.2-stable
 | 
				
			||||||
      - name: Push Docker Container to Registry (latest)
 | 
					      - name: Push Docker Container to Registry (latest)
 | 
				
			||||||
        run: docker push beryju/authentik-static:latest
 | 
					        run: docker push beryju/authentik-static:latest
 | 
				
			||||||
  test-release:
 | 
					  test-release:
 | 
				
			||||||
@ -107,5 +107,5 @@ jobs:
 | 
				
			|||||||
          SENTRY_PROJECT: authentik
 | 
					          SENTRY_PROJECT: authentik
 | 
				
			||||||
          SENTRY_URL: https://sentry.beryju.org
 | 
					          SENTRY_URL: https://sentry.beryju.org
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          tagName: 0.14.2-stable
 | 
					          tagName: 2021.1.2-stable
 | 
				
			||||||
          environment: beryjuorg-prod
 | 
					          environment: beryjuorg-prod
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										69
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										69
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -74,18 +74,18 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "boto3": {
 | 
					        "boto3": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:197926eaf0065c2c503914a15edc75f4ac259c1e5ae6d17eabd1ba5d8ebd1554",
 | 
					                "sha256:3f26aad4c6b238055d17fd662620284ffb4ced542ed9a2f7f9df65d97a3f1190",
 | 
				
			||||||
                "sha256:d6991e6fd7d0f63bf94282687700a91f5299b807e544cb3367e9b2faeeaf8c62"
 | 
					                "sha256:47151ed571c316458f4931cd2422995ba0c9f6818c5df7d75f49fc845208e42e"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.16.46"
 | 
					            "version": "==1.16.56"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "botocore": {
 | 
					        "botocore": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:85ca6915ad5471e7f6cd1b00610b74601d2970cbf8e9b1bf255697154cf621a3",
 | 
					                "sha256:01496e4c2c06aab79689f2c345a0e2cceb5fe1da7858a7e7df189bcf97703223",
 | 
				
			||||||
                "sha256:f7d365c689070368a5a0857aa35a81d7c950556189f23065f42798f810a59cae"
 | 
					                "sha256:a37d073c2f166753cc3799e77d87d4096e24433fcca5e7c8cc8e77e5dbfe60e9"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==1.19.46"
 | 
					            "version": "==1.19.56"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "cachetools": {
 | 
					        "cachetools": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -265,11 +265,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "django": {
 | 
					        "django": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
 | 
					                "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
 | 
				
			||||||
                "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
 | 
					                "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==3.1.4"
 | 
					            "version": "==3.1.5"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "django-cors-middleware": {
 | 
					        "django-cors-middleware": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -351,7 +351,8 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "djangorestframework": {
 | 
					        "djangorestframework": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"
 | 
					                "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7",
 | 
				
			||||||
 | 
					                "sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==3.12.2"
 | 
					            "version": "==3.12.2"
 | 
				
			||||||
@ -411,10 +412,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "h11": {
 | 
					        "h11": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
 | 
					                "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
 | 
				
			||||||
                "sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
 | 
					                "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==0.11.0"
 | 
					            "version": "==0.12.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "hiredis": {
 | 
					        "hiredis": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -486,10 +487,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "hyperlink": {
 | 
					        "hyperlink": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
 | 
					                "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b",
 | 
				
			||||||
                "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63"
 | 
					                "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==20.0.1"
 | 
					            "version": "==21.0.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "idna": {
 | 
					        "idna": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -701,10 +702,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "prompt-toolkit": {
 | 
					        "prompt-toolkit": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
 | 
					                "sha256:ac329c69bd8564cb491940511957312c7b8959bb5b3cf3582b406068a51d5bb7",
 | 
				
			||||||
                "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
 | 
					                "sha256:b8b3d0bde65da350290c46a8f54f336b3cbf5464a4ac11239668d986852e79d5"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==3.0.8"
 | 
					            "version": "==3.0.10"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "psycopg2-binary": {
 | 
					        "psycopg2-binary": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -955,11 +956,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "rsa": {
 | 
					        "rsa": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
 | 
					                "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4",
 | 
				
			||||||
                "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
 | 
					                "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3.6'",
 | 
					            "markers": "python_version >= '3.6'",
 | 
				
			||||||
            "version": "==4.6"
 | 
					            "version": "==4.7"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "ruamel.yaml": {
 | 
					        "ruamel.yaml": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -970,10 +971,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "s3transfer": {
 | 
					        "s3transfer": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
 | 
					                "sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed",
 | 
				
			||||||
                "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
 | 
					                "sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==0.3.3"
 | 
					            "version": "==0.3.4"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "sentry-sdk": {
 | 
					        "sentry-sdk": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1007,11 +1008,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "structlog": {
 | 
					        "structlog": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b",
 | 
					                "sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd",
 | 
				
			||||||
                "sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92"
 | 
					                "sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==20.1.0"
 | 
					            "version": "==20.2.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "swagger-spec-validator": {
 | 
					        "swagger-spec-validator": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1373,11 +1374,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "django": {
 | 
					        "django": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
 | 
					                "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
 | 
				
			||||||
                "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
 | 
					                "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==3.1.4"
 | 
					            "version": "==3.1.5"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "django-debug-toolbar": {
 | 
					        "django-debug-toolbar": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1417,10 +1418,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "gitpython": {
 | 
					        "gitpython": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
 | 
					                "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac",
 | 
				
			||||||
                "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
 | 
					                "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==3.1.11"
 | 
					            "version": "==3.1.12"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "iniconfig": {
 | 
					        "iniconfig": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								SECURITY.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								SECURITY.md
									
									
									
									
									
								
							@ -2,13 +2,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Supported Versions
 | 
					## Supported Versions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
As authentik is currently in a pre-stable, only the latest "stable" version is supported. After authentik 1.0, this will change.
 | 
					| Version    | Supported          |
 | 
				
			||||||
 | 
					| ---------- | ------------------ |
 | 
				
			||||||
| Version  | Supported          |
 | 
					| 0.13.x     | :white_check_mark: |
 | 
				
			||||||
| -------- | ------------------ |
 | 
					| 0.14.x     | :white_check_mark: |
 | 
				
			||||||
| 0.12.x   | :white_check_mark: |
 | 
					| 2021.1.x   | :white_check_mark: |
 | 
				
			||||||
| 0.13.x   | :white_check_mark: |
 | 
					 | 
				
			||||||
| 0.14.x   | :white_check_mark: |
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Reporting a Vulnerability
 | 
					## Reporting a Vulnerability
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,2 @@
 | 
				
			|||||||
"""authentik"""
 | 
					"""authentik"""
 | 
				
			||||||
__version__ = "0.14.2-stable"
 | 
					__version__ = "2021.1.2-stable"
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.serializers import Serializer
 | 
					from rest_framework.serializers import Serializer
 | 
				
			||||||
from rest_framework.viewsets import ViewSet
 | 
					from rest_framework.viewsets import ViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.tasks import TaskInfo
 | 
					from authentik.events.monitored_tasks import TaskInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TaskSerializer(Serializer):
 | 
					class TaskSerializer(Serializer):
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,11 @@
 | 
				
			|||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from packaging.version import parse
 | 
					from packaging.version import parse
 | 
				
			||||||
from requests import RequestException, get
 | 
					from requests import RequestException, get
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,7 @@
 | 
				
			|||||||
                {% for task in object_list %}
 | 
					                {% for task in object_list %}
 | 
				
			||||||
                <tr role="row">
 | 
					                <tr role="row">
 | 
				
			||||||
                    <th role="columnheader">
 | 
					                    <th role="columnheader">
 | 
				
			||||||
                        <pre>{{ task.task_name }}</pre>
 | 
					                        <span>{{ task.html_name|join:"_­" }}</span>
 | 
				
			||||||
                    </th>
 | 
					                    </th>
 | 
				
			||||||
                    <td role="cell">
 | 
					                    <td role="cell">
 | 
				
			||||||
                        <span>
 | 
					                        <span>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from django import template
 | 
					from django import template
 | 
				
			||||||
from django.db.models import Model
 | 
					from django.db.models import Model
 | 
				
			||||||
from django.utils.html import mark_safe
 | 
					from django.utils.html import mark_safe
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
register = template.Library()
 | 
					register = template.Library()
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ REQUEST_MOCK_VALID = Mock(
 | 
				
			|||||||
    return_value=MockResponse(
 | 
					    return_value=MockResponse(
 | 
				
			||||||
        200,
 | 
					        200,
 | 
				
			||||||
        """{
 | 
					        """{
 | 
				
			||||||
            "tag_name": "version/1.2.3"
 | 
					            "tag_name": "version/99999999.9999999"
 | 
				
			||||||
        }""",
 | 
					        }""",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -47,10 +47,11 @@ class TestAdminTasks(TestCase):
 | 
				
			|||||||
    def test_version_valid_response(self):
 | 
					    def test_version_valid_response(self):
 | 
				
			||||||
        """Test Update checker with valid response"""
 | 
					        """Test Update checker with valid response"""
 | 
				
			||||||
        update_latest_version.delay().get()
 | 
					        update_latest_version.delay().get()
 | 
				
			||||||
        self.assertEqual(cache.get(VERSION_CACHE_KEY), "1.2.3")
 | 
					        self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
 | 
				
			||||||
        self.assertTrue(
 | 
					        self.assertTrue(
 | 
				
			||||||
            Event.objects.filter(
 | 
					            Event.objects.filter(
 | 
				
			||||||
                action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
 | 
					                action=EventAction.UPDATE_AVAILABLE,
 | 
				
			||||||
 | 
					                context__new_version="99999999.9999999",
 | 
				
			||||||
            ).exists()
 | 
					            ).exists()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # test that a consecutive check doesn't create a duplicate event
 | 
					        # test that a consecutive check doesn't create a duplicate event
 | 
				
			||||||
@ -58,7 +59,8 @@ class TestAdminTasks(TestCase):
 | 
				
			|||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            len(
 | 
					            len(
 | 
				
			||||||
                Event.objects.filter(
 | 
					                Event.objects.filter(
 | 
				
			||||||
                    action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
 | 
					                    action=EventAction.UPDATE_AVAILABLE,
 | 
				
			||||||
 | 
					                    context__new_version="99999999.9999999",
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            1,
 | 
					            1,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ from django.urls import path
 | 
				
			|||||||
from authentik.admin.views import (
 | 
					from authentik.admin.views import (
 | 
				
			||||||
    applications,
 | 
					    applications,
 | 
				
			||||||
    certificate_key_pair,
 | 
					    certificate_key_pair,
 | 
				
			||||||
 | 
					    events_notifications_rules,
 | 
				
			||||||
 | 
					    events_notifications_transports,
 | 
				
			||||||
    flows,
 | 
					    flows,
 | 
				
			||||||
    groups,
 | 
					    groups,
 | 
				
			||||||
    outposts,
 | 
					    outposts,
 | 
				
			||||||
@ -352,4 +354,36 @@ urlpatterns = [
 | 
				
			|||||||
        tasks.TaskListView.as_view(),
 | 
					        tasks.TaskListView.as_view(),
 | 
				
			||||||
        name="tasks",
 | 
					        name="tasks",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    # Event Notification Transpots
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "events/transports/create/",
 | 
				
			||||||
 | 
					        events_notifications_transports.NotificationTransportCreateView.as_view(),
 | 
				
			||||||
 | 
					        name="notification-transport-create",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "events/transports/<uuid:pk>/update/",
 | 
				
			||||||
 | 
					        events_notifications_transports.NotificationTransportUpdateView.as_view(),
 | 
				
			||||||
 | 
					        name="notification-transport-update",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "events/transports/<uuid:pk>/delete/",
 | 
				
			||||||
 | 
					        events_notifications_transports.NotificationTransportDeleteView.as_view(),
 | 
				
			||||||
 | 
					        name="notification-transport-delete",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    # Event Notification Rules
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "events/rules/create/",
 | 
				
			||||||
 | 
					        events_notifications_rules.NotificationRuleCreateView.as_view(),
 | 
				
			||||||
 | 
					        name="notification-rule-create",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "events/rules/<uuid:pk>/update/",
 | 
				
			||||||
 | 
					        events_notifications_rules.NotificationRuleUpdateView.as_view(),
 | 
				
			||||||
 | 
					        name="notification-rule-update",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "events/rules/<uuid:pk>/delete/",
 | 
				
			||||||
 | 
					        events_notifications_rules.NotificationRuleDeleteView.as_view(),
 | 
				
			||||||
 | 
					        name="notification-rule-delete",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										64
									
								
								authentik/admin/views/events_notifications_rules.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								authentik/admin/views/events_notifications_rules.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					"""authentik NotificationRule administration"""
 | 
				
			||||||
 | 
					from django.contrib.auth.mixins import LoginRequiredMixin
 | 
				
			||||||
 | 
					from django.contrib.auth.mixins import (
 | 
				
			||||||
 | 
					    PermissionRequiredMixin as DjangoPermissionRequiredMixin,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from django.contrib.messages.views import SuccessMessageMixin
 | 
				
			||||||
 | 
					from django.urls import reverse_lazy
 | 
				
			||||||
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
 | 
					from django.views.generic import UpdateView
 | 
				
			||||||
 | 
					from guardian.mixins import PermissionRequiredMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
 | 
				
			||||||
 | 
					from authentik.events.forms import NotificationRuleForm
 | 
				
			||||||
 | 
					from authentik.events.models import NotificationRule
 | 
				
			||||||
 | 
					from authentik.lib.views import CreateAssignPermView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRuleCreateView(
 | 
				
			||||||
 | 
					    SuccessMessageMixin,
 | 
				
			||||||
 | 
					    BackSuccessUrlMixin,
 | 
				
			||||||
 | 
					    LoginRequiredMixin,
 | 
				
			||||||
 | 
					    DjangoPermissionRequiredMixin,
 | 
				
			||||||
 | 
					    CreateAssignPermView,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Create new NotificationRule"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = NotificationRule
 | 
				
			||||||
 | 
					    form_class = NotificationRuleForm
 | 
				
			||||||
 | 
					    permission_required = "authentik_events.add_NotificationRule"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template_name = "generic/create.html"
 | 
				
			||||||
 | 
					    success_url = reverse_lazy("authentik_core:shell")
 | 
				
			||||||
 | 
					    success_message = _("Successfully created Notification Rule")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRuleUpdateView(
 | 
				
			||||||
 | 
					    SuccessMessageMixin,
 | 
				
			||||||
 | 
					    BackSuccessUrlMixin,
 | 
				
			||||||
 | 
					    LoginRequiredMixin,
 | 
				
			||||||
 | 
					    PermissionRequiredMixin,
 | 
				
			||||||
 | 
					    UpdateView,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Update application"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = NotificationRule
 | 
				
			||||||
 | 
					    form_class = NotificationRuleForm
 | 
				
			||||||
 | 
					    permission_required = "authentik_events.change_NotificationRule"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template_name = "generic/update.html"
 | 
				
			||||||
 | 
					    success_url = reverse_lazy("authentik_core:shell")
 | 
				
			||||||
 | 
					    success_message = _("Successfully updated Notification Rule")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRuleDeleteView(
 | 
				
			||||||
 | 
					    LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Delete application"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = NotificationRule
 | 
				
			||||||
 | 
					    permission_required = "authentik_events.delete_NotificationRule"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template_name = "generic/delete.html"
 | 
				
			||||||
 | 
					    success_url = reverse_lazy("authentik_core:shell")
 | 
				
			||||||
 | 
					    success_message = _("Successfully deleted Notification Rule")
 | 
				
			||||||
							
								
								
									
										64
									
								
								authentik/admin/views/events_notifications_transports.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								authentik/admin/views/events_notifications_transports.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					"""authentik NotificationTransport administration"""
 | 
				
			||||||
 | 
					from django.contrib.auth.mixins import LoginRequiredMixin
 | 
				
			||||||
 | 
					from django.contrib.auth.mixins import (
 | 
				
			||||||
 | 
					    PermissionRequiredMixin as DjangoPermissionRequiredMixin,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from django.contrib.messages.views import SuccessMessageMixin
 | 
				
			||||||
 | 
					from django.urls import reverse_lazy
 | 
				
			||||||
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
 | 
					from django.views.generic import UpdateView
 | 
				
			||||||
 | 
					from guardian.mixins import PermissionRequiredMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
 | 
				
			||||||
 | 
					from authentik.events.forms import NotificationTransportForm
 | 
				
			||||||
 | 
					from authentik.events.models import NotificationTransport
 | 
				
			||||||
 | 
					from authentik.lib.views import CreateAssignPermView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportCreateView(
 | 
				
			||||||
 | 
					    SuccessMessageMixin,
 | 
				
			||||||
 | 
					    BackSuccessUrlMixin,
 | 
				
			||||||
 | 
					    LoginRequiredMixin,
 | 
				
			||||||
 | 
					    DjangoPermissionRequiredMixin,
 | 
				
			||||||
 | 
					    CreateAssignPermView,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Create new NotificationTransport"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = NotificationTransport
 | 
				
			||||||
 | 
					    form_class = NotificationTransportForm
 | 
				
			||||||
 | 
					    permission_required = "authentik_events.add_notificationtransport"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template_name = "generic/create.html"
 | 
				
			||||||
 | 
					    success_url = reverse_lazy("authentik_core:shell")
 | 
				
			||||||
 | 
					    success_message = _("Successfully created Notification Transport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportUpdateView(
 | 
				
			||||||
 | 
					    SuccessMessageMixin,
 | 
				
			||||||
 | 
					    BackSuccessUrlMixin,
 | 
				
			||||||
 | 
					    LoginRequiredMixin,
 | 
				
			||||||
 | 
					    PermissionRequiredMixin,
 | 
				
			||||||
 | 
					    UpdateView,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Update application"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = NotificationTransport
 | 
				
			||||||
 | 
					    form_class = NotificationTransportForm
 | 
				
			||||||
 | 
					    permission_required = "authentik_events.change_notificationtransport"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template_name = "generic/update.html"
 | 
				
			||||||
 | 
					    success_url = reverse_lazy("authentik_core:shell")
 | 
				
			||||||
 | 
					    success_message = _("Successfully updated Notification Transport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportDeleteView(
 | 
				
			||||||
 | 
					    LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Delete application"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = NotificationTransport
 | 
				
			||||||
 | 
					    permission_required = "authentik_events.delete_notificationtransport"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template_name = "generic/delete.html"
 | 
				
			||||||
 | 
					    success_url = reverse_lazy("authentik_core:shell")
 | 
				
			||||||
 | 
					    success_message = _("Successfully deleted Notification Transport")
 | 
				
			||||||
@ -5,10 +5,11 @@ from django.http.request import HttpRequest
 | 
				
			|||||||
from django.http.response import HttpResponse
 | 
					from django.http.response import HttpResponse
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from django.views.generic import FormView
 | 
					from django.views.generic import FormView
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
 | 
					from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
 | 
				
			||||||
from authentik.admin.mixins import AdminRequiredMixin
 | 
					from authentik.admin.mixins import AdminRequiredMixin
 | 
				
			||||||
 | 
					from authentik.core.api.applications import user_app_cache_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,6 +27,9 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
 | 
				
			|||||||
        keys = cache.keys("policy_*")
 | 
					        keys = cache.keys("policy_*")
 | 
				
			||||||
        cache.delete_many(keys)
 | 
					        cache.delete_many(keys)
 | 
				
			||||||
        LOGGER.debug("Cleared Policy cache", keys=len(keys))
 | 
					        LOGGER.debug("Cleared Policy cache", keys=len(keys))
 | 
				
			||||||
 | 
					        # Also delete user application cache
 | 
				
			||||||
 | 
					        keys = user_app_cache_key("*")
 | 
				
			||||||
 | 
					        cache.delete_many(keys)
 | 
				
			||||||
        return super().post(request, *args, **kwargs)
 | 
					        return super().post(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ from typing import Any, Dict
 | 
				
			|||||||
from django.views.generic.base import TemplateView
 | 
					from django.views.generic.base import TemplateView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.mixins import AdminRequiredMixin
 | 
					from authentik.admin.mixins import AdminRequiredMixin
 | 
				
			||||||
from authentik.lib.tasks import TaskInfo, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TaskListView(AdminRequiredMixin, TemplateView):
 | 
					class TaskListView(AdminRequiredMixin, TemplateView):
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ from typing import Any, Optional, Tuple, Union
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
 | 
					from rest_framework.authentication import BaseAuthentication, get_authorization_header
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from structlog 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,10 @@ from authentik.core.api.sources import SourceViewSet
 | 
				
			|||||||
from authentik.core.api.tokens import TokenViewSet
 | 
					from authentik.core.api.tokens import TokenViewSet
 | 
				
			||||||
from authentik.core.api.users import UserViewSet
 | 
					from authentik.core.api.users import UserViewSet
 | 
				
			||||||
from authentik.crypto.api import CertificateKeyPairViewSet
 | 
					from authentik.crypto.api import CertificateKeyPairViewSet
 | 
				
			||||||
from authentik.events.api import EventViewSet
 | 
					from authentik.events.api.event import EventViewSet
 | 
				
			||||||
 | 
					from authentik.events.api.notification import NotificationViewSet
 | 
				
			||||||
 | 
					from authentik.events.api.notification_rule import NotificationRuleViewSet
 | 
				
			||||||
 | 
					from authentik.events.api.notification_transport import NotificationTransportViewSet
 | 
				
			||||||
from authentik.flows.api import (
 | 
					from authentik.flows.api import (
 | 
				
			||||||
    FlowCacheViewSet,
 | 
					    FlowCacheViewSet,
 | 
				
			||||||
    FlowStageBindingViewSet,
 | 
					    FlowStageBindingViewSet,
 | 
				
			||||||
@ -37,6 +40,7 @@ from authentik.policies.api import (
 | 
				
			|||||||
    PolicyViewSet,
 | 
					    PolicyViewSet,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.policies.dummy.api import DummyPolicyViewSet
 | 
					from authentik.policies.dummy.api import DummyPolicyViewSet
 | 
				
			||||||
 | 
					from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
 | 
				
			||||||
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
 | 
					from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
 | 
				
			||||||
from authentik.policies.expression.api import ExpressionPolicyViewSet
 | 
					from authentik.policies.expression.api import ExpressionPolicyViewSet
 | 
				
			||||||
from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet
 | 
					from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet
 | 
				
			||||||
@ -97,6 +101,9 @@ router.register("flows/bindings", FlowStageBindingViewSet)
 | 
				
			|||||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
 | 
					router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("events/events", EventViewSet)
 | 
					router.register("events/events", EventViewSet)
 | 
				
			||||||
 | 
					router.register("events/notifications", NotificationViewSet)
 | 
				
			||||||
 | 
					router.register("events/transports", NotificationTransportViewSet)
 | 
				
			||||||
 | 
					router.register("events/rules", NotificationRuleViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("sources/all", SourceViewSet)
 | 
					router.register("sources/all", SourceViewSet)
 | 
				
			||||||
router.register("sources/ldap", LDAPSourceViewSet)
 | 
					router.register("sources/ldap", LDAPSourceViewSet)
 | 
				
			||||||
@ -107,6 +114,7 @@ router.register("policies/all", PolicyViewSet)
 | 
				
			|||||||
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
 | 
					router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
 | 
				
			||||||
router.register("policies/bindings", PolicyBindingViewSet)
 | 
					router.register("policies/bindings", PolicyBindingViewSet)
 | 
				
			||||||
router.register("policies/expression", ExpressionPolicyViewSet)
 | 
					router.register("policies/expression", ExpressionPolicyViewSet)
 | 
				
			||||||
 | 
					router.register("policies/event_matcher", EventMatcherPolicyViewSet)
 | 
				
			||||||
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
 | 
					router.register("policies/group_membership", GroupMembershipPolicyViewSet)
 | 
				
			||||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
 | 
					router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
 | 
				
			||||||
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
 | 
					router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ from django.apps import AppConfig, apps
 | 
				
			|||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
from django.contrib.admin.sites import AlreadyRegistered
 | 
					from django.contrib.admin.sites import AlreadyRegistered
 | 
				
			||||||
from guardian.admin import GuardedModelAdmin
 | 
					from guardian.admin import GuardedModelAdmin
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
"""Application API Views"""
 | 
					"""Application API Views"""
 | 
				
			||||||
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.db.models import QuerySet
 | 
					from django.db.models import QuerySet
 | 
				
			||||||
from django.http.response import Http404
 | 
					from django.http.response import Http404
 | 
				
			||||||
from guardian.shortcuts import get_objects_for_user
 | 
					from guardian.shortcuts import get_objects_for_user
 | 
				
			||||||
@ -18,6 +19,11 @@ from authentik.events.models import EventAction
 | 
				
			|||||||
from authentik.policies.engine import PolicyEngine
 | 
					from authentik.policies.engine import PolicyEngine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def user_app_cache_key(user_pk: str) -> str:
 | 
				
			||||||
 | 
					    """Cache key where application list for user is saved"""
 | 
				
			||||||
 | 
					    return f"user_app_cache_{user_pk}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApplicationSerializer(ModelSerializer):
 | 
					class ApplicationSerializer(ModelSerializer):
 | 
				
			||||||
    """Application Serializer"""
 | 
					    """Application Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -72,12 +78,19 @@ class ApplicationViewSet(ModelViewSet):
 | 
				
			|||||||
        """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())
 | 
				
			||||||
        self.paginate_queryset(queryset)
 | 
					        self.paginate_queryset(queryset)
 | 
				
			||||||
        allowed_applications = []
 | 
					        allowed_applications = cache.get(user_app_cache_key(self.request.user.pk))
 | 
				
			||||||
        for application in queryset:
 | 
					        if not allowed_applications:
 | 
				
			||||||
            engine = PolicyEngine(application, self.request.user, self.request)
 | 
					            allowed_applications = []
 | 
				
			||||||
            engine.build()
 | 
					            for application in queryset:
 | 
				
			||||||
            if engine.passing:
 | 
					                engine = PolicyEngine(application, self.request.user, self.request)
 | 
				
			||||||
                allowed_applications.append(application)
 | 
					                engine.build()
 | 
				
			||||||
 | 
					                if engine.passing:
 | 
				
			||||||
 | 
					                    allowed_applications.append(application)
 | 
				
			||||||
 | 
					            cache.set(
 | 
				
			||||||
 | 
					                user_app_cache_key(self.request.user.pk),
 | 
				
			||||||
 | 
					                allowed_applications,
 | 
				
			||||||
 | 
					                timeout=86400,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        serializer = self.get_serializer(allowed_applications, many=True)
 | 
					        serializer = self.get_serializer(allowed_applications, many=True)
 | 
				
			||||||
        return self.get_paginated_response(serializer.data)
 | 
					        return self.get_paginated_response(serializer.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@ class PropertyMappingSerializer(ModelSerializer):
 | 
				
			|||||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
 | 
					class PropertyMappingViewSet(ReadOnlyModelViewSet):
 | 
				
			||||||
    """PropertyMapping Viewset"""
 | 
					    """PropertyMapping Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = PropertyMapping.objects.all()
 | 
					    queryset = PropertyMapping.objects.none()
 | 
				
			||||||
    serializer_class = PropertyMappingSerializer
 | 
					    serializer_class = PropertyMappingSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
 | 
				
			|||||||
class ProviderViewSet(ModelViewSet):
 | 
					class ProviderViewSet(ModelViewSet):
 | 
				
			||||||
    """Provider Viewset"""
 | 
					    """Provider Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = Provider.objects.all()
 | 
					    queryset = Provider.objects.none()
 | 
				
			||||||
    serializer_class = ProviderSerializer
 | 
					    serializer_class = ProviderSerializer
 | 
				
			||||||
    filterset_fields = {
 | 
					    filterset_fields = {
 | 
				
			||||||
        "application": ["isnull"],
 | 
					        "application": ["isnull"],
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
 | 
				
			|||||||
class SourceViewSet(ReadOnlyModelViewSet):
 | 
					class SourceViewSet(ReadOnlyModelViewSet):
 | 
				
			||||||
    """Source Viewset"""
 | 
					    """Source Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = Source.objects.all()
 | 
					    queryset = Source.objects.none()
 | 
				
			||||||
    serializer_class = SourceSerializer
 | 
					    serializer_class = SourceSerializer
 | 
				
			||||||
    lookup_field = "slug"
 | 
					    lookup_field = "slug"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ class UserSerializer(ModelSerializer):
 | 
				
			|||||||
class UserViewSet(ModelViewSet):
 | 
					class UserViewSet(ModelViewSet):
 | 
				
			||||||
    """User Viewset"""
 | 
					    """User Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = User.objects.all()
 | 
					    queryset = User.objects.none()
 | 
				
			||||||
    serializer_class = UserSerializer
 | 
					    serializer_class = UserSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +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 structlog 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
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
 | 
				
			|||||||
        event = Event.new(
 | 
					        event = Event.new(
 | 
				
			||||||
            EventAction.PROPERTY_MAPPING_EXCEPTION,
 | 
					            EventAction.PROPERTY_MAPPING_EXCEPTION,
 | 
				
			||||||
            expression=expression_source,
 | 
					            expression=expression_source,
 | 
				
			||||||
            error=error_string,
 | 
					            message=error_string,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if "user" in self._context:
 | 
					        if "user" in self._context:
 | 
				
			||||||
            event.set_user(self._context["user"])
 | 
					            event.set_user(self._context["user"])
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _
 | 
				
			|||||||
from guardian.mixins import GuardianUserMixin
 | 
					from guardian.mixins import GuardianUserMixin
 | 
				
			||||||
from model_utils.managers import InheritanceManager
 | 
					from model_utils.managers import InheritanceManager
 | 
				
			||||||
from rest_framework.serializers import Serializer
 | 
					from rest_framework.serializers import Serializer
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.exceptions import PropertyMappingExpressionException
 | 
					from authentik.core.exceptions import PropertyMappingExpressionException
 | 
				
			||||||
from authentik.core.signals import password_changed
 | 
					from authentik.core.signals import password_changed
 | 
				
			||||||
 | 
				
			|||||||
@ -8,10 +8,10 @@ from dbbackup.db.exceptions import CommandConnectorError
 | 
				
			|||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
 | 
					from django.contrib.humanize.templatetags.humanize import naturaltime
 | 
				
			||||||
from django.core import management
 | 
					from django.core import management
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import ExpiringModel
 | 
					from authentik.core.models import ExpiringModel
 | 
				
			||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
				
			|||||||
@ -9,14 +9,14 @@
 | 
				
			|||||||
        <meta charset="UTF-8">
 | 
					        <meta charset="UTF-8">
 | 
				
			||||||
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 | 
					        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 | 
				
			||||||
        <title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
 | 
					        <title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
 | 
				
			||||||
        <link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
 | 
					        <link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
 | 
				
			||||||
        <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
 | 
					        <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}">
 | 
					        <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}?v={{ ak_version }}">
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}">
 | 
					        <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}?v={{ ak_version }}">
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}">
 | 
					        <link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}?v={{ ak_version }}">
 | 
				
			||||||
        <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
 | 
					        <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
 | 
				
			||||||
        <script src="{% url 'javascript-catalog' %}"></script>
 | 
					        <script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script>
 | 
				
			||||||
        <script src="{% static 'dist/main.js' %}" type="module"></script>
 | 
					        <script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script>
 | 
				
			||||||
        {% block head %}
 | 
					        {% block head %}
 | 
				
			||||||
        {% endblock %}
 | 
					        {% endblock %}
 | 
				
			||||||
    </head>
 | 
					    </head>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
{% load i18n %}
 | 
					{% load i18n %}
 | 
				
			||||||
{% load authentik_user_settings %}
 | 
					{% load authentik_user_settings %}
 | 
				
			||||||
 | 
					{% load authentik_utils %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="pf-c-page">
 | 
					<div class="pf-c-page">
 | 
				
			||||||
    <main role="main" class="pf-c-page__main" tabindex="-1">
 | 
					    <main role="main" class="pf-c-page__main" tabindex="-1">
 | 
				
			||||||
@ -12,47 +13,45 @@
 | 
				
			|||||||
                <p>{% trans "Configure settings relevant to your user profile." %}</p>
 | 
					                <p>{% trans "Configure settings relevant to your user profile." %}</p>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </section>
 | 
					        </section>
 | 
				
			||||||
        <section class="pf-c-page__main-section">
 | 
					        <ak-tabs>
 | 
				
			||||||
            <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
					            <section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
				
			||||||
                <div class="pf-u-w-75">
 | 
					                <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
				
			||||||
                    <ak-site-shell url="{% url 'authentik_core:user-details' %}">
 | 
					                    <div class="pf-u-w-75">
 | 
				
			||||||
                        <div slot="body"></div>
 | 
					                        <ak-site-shell url="{% url 'authentik_core:user-details' %}">
 | 
				
			||||||
                    </ak-site-shell>
 | 
					                            <div slot="body"></div>
 | 
				
			||||||
 | 
					                        </ak-site-shell>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </section>
 | 
				
			||||||
        </section>
 | 
					            <section slot="page-2" data-tab-title="{% trans 'Tokens' %}" class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
				
			||||||
        <section class="pf-c-page__main-section">
 | 
					                <ak-site-shell url="{% url 'authentik_core:user-tokens' %}">
 | 
				
			||||||
            <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
					                    <div slot="body"></div>
 | 
				
			||||||
                <div class="pf-u-w-75">
 | 
					                </ak-site-shell>
 | 
				
			||||||
                    <ak-site-shell url="{% url 'authentik_core:user-tokens' %}">
 | 
					            </section>
 | 
				
			||||||
                        <div slot="body"></div>
 | 
					            {% user_stages as user_stages_loc %}
 | 
				
			||||||
                    </ak-site-shell>
 | 
					            {% for stage, stage_link in user_stages_loc.items %}
 | 
				
			||||||
 | 
					            <section slot="page-{{ stage.pk }}" data-tab-title="{{ stage|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
				
			||||||
 | 
					                <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
				
			||||||
 | 
					                    <div class="pf-u-w-75">
 | 
				
			||||||
 | 
					                        <ak-site-shell url="{{ stage_link }}">
 | 
				
			||||||
 | 
					                            <div slot="body"></div>
 | 
				
			||||||
 | 
					                        </ak-site-shell>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </section>
 | 
				
			||||||
        </section>
 | 
					            {% endfor %}
 | 
				
			||||||
        {% user_stages as user_stages_loc %}
 | 
					            {% user_sources as user_sources_loc %}
 | 
				
			||||||
        {% for stage in user_stages_loc %}
 | 
					            {% for source, source_link in user_sources_loc.item %}
 | 
				
			||||||
        <section class="pf-c-page__main-section">
 | 
					            <section slot="page-{{ source.pk }}" data-tab-title="{{ source|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
				
			||||||
            <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
					                <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
				
			||||||
                <div class="pf-u-w-75">
 | 
					                    <div class="pf-u-w-75">
 | 
				
			||||||
                    <ak-site-shell url="{{ stage }}">
 | 
					                        <ak-site-shell url="{{ source_link }}">
 | 
				
			||||||
                        <div slot="body"></div>
 | 
					                            <div slot="body"></div>
 | 
				
			||||||
                    </ak-site-shell>
 | 
					                        </ak-site-shell>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </section>
 | 
				
			||||||
        </section>
 | 
					            {% endfor %}
 | 
				
			||||||
        {% endfor %}
 | 
					        </ak-tabs>
 | 
				
			||||||
        {% user_sources as user_sources_loc %}
 | 
					 | 
				
			||||||
        {% for source in user_sources_loc %}
 | 
					 | 
				
			||||||
        <section class="pf-c-page__main-section">
 | 
					 | 
				
			||||||
            <div class="pf-u-display-flex pf-u-justify-content-center">
 | 
					 | 
				
			||||||
                <div class="pf-u-w-75">
 | 
					 | 
				
			||||||
                    <ak-site-shell url="{{ source }}">
 | 
					 | 
				
			||||||
                        <div slot="body"></div>
 | 
					 | 
				
			||||||
                    </ak-site-shell>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </section>
 | 
					 | 
				
			||||||
        {% endfor %}
 | 
					 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -13,26 +13,26 @@ register = template.Library()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@register.simple_tag(takes_context=True)
 | 
					@register.simple_tag(takes_context=True)
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def user_stages(context: RequestContext) -> list[str]:
 | 
					def user_stages(context: RequestContext) -> dict[Stage, str]:
 | 
				
			||||||
    """Return list of all stages which apply to user"""
 | 
					    """Return list of all stages which apply to user"""
 | 
				
			||||||
    _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
 | 
					    _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
 | 
				
			||||||
    matching_stages: list[str] = []
 | 
					    matching_stages: dict[Stage, str] = {}
 | 
				
			||||||
    for stage in _all_stages:
 | 
					    for stage in _all_stages:
 | 
				
			||||||
        user_settings = stage.ui_user_settings
 | 
					        user_settings = stage.ui_user_settings
 | 
				
			||||||
        if not user_settings:
 | 
					        if not user_settings:
 | 
				
			||||||
            continue
 | 
					            continue
 | 
				
			||||||
        matching_stages.append(user_settings)
 | 
					        matching_stages[stage] = user_settings
 | 
				
			||||||
    return matching_stages
 | 
					    return matching_stages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@register.simple_tag(takes_context=True)
 | 
					@register.simple_tag(takes_context=True)
 | 
				
			||||||
def user_sources(context: RequestContext) -> list[str]:
 | 
					def user_sources(context: RequestContext) -> dict[Source, str]:
 | 
				
			||||||
    """Return a list of all sources which are enabled for the user"""
 | 
					    """Return a list of all sources which are enabled for the user"""
 | 
				
			||||||
    user = context.get("request").user
 | 
					    user = context.get("request").user
 | 
				
			||||||
    _all_sources: Iterable[Source] = Source.objects.filter(
 | 
					    _all_sources: Iterable[Source] = Source.objects.filter(
 | 
				
			||||||
        enabled=True
 | 
					        enabled=True
 | 
				
			||||||
    ).select_subclasses()
 | 
					    ).select_subclasses()
 | 
				
			||||||
    matching_sources: list[str] = []
 | 
					    matching_sources: dict[Source, str] = {}
 | 
				
			||||||
    for source in _all_sources:
 | 
					    for source in _all_sources:
 | 
				
			||||||
        user_settings = source.ui_user_settings
 | 
					        user_settings = source.ui_user_settings
 | 
				
			||||||
        if not user_settings:
 | 
					        if not user_settings:
 | 
				
			||||||
@ -40,5 +40,5 @@ def user_sources(context: RequestContext) -> list[str]:
 | 
				
			|||||||
        policy_engine = PolicyEngine(source, user, context.get("request"))
 | 
					        policy_engine = PolicyEngine(source, user, context.get("request"))
 | 
				
			||||||
        policy_engine.build()
 | 
					        policy_engine.build()
 | 
				
			||||||
        if policy_engine.passing:
 | 
					        if policy_engine.passing:
 | 
				
			||||||
            matching_sources.append(user_settings)
 | 
					            matching_sources[source] = user_settings
 | 
				
			||||||
    return matching_sources
 | 
					    return matching_sources
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.shortcuts import get_object_or_404, redirect
 | 
					from django.shortcuts import get_object_or_404, redirect
 | 
				
			||||||
from django.views import View
 | 
					from django.views import View
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.middleware import (
 | 
					from authentik.core.middleware import (
 | 
				
			||||||
    SESSION_IMPERSONATE_ORIGINAL_USER,
 | 
					    SESSION_IMPERSONATE_ORIGINAL_USER,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								authentik/events/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/events/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										53
									
								
								authentik/events/api/notification.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								authentik/events/api/notification.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					"""Notification API Views"""
 | 
				
			||||||
 | 
					from rest_framework import mixins
 | 
				
			||||||
 | 
					from rest_framework.fields import ReadOnlyField
 | 
				
			||||||
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
 | 
					from rest_framework.viewsets import GenericViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.api.event import EventSerializer
 | 
				
			||||||
 | 
					from authentik.events.models import Notification
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    """Notification Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    body = ReadOnlyField()
 | 
				
			||||||
 | 
					    severity = ReadOnlyField()
 | 
				
			||||||
 | 
					    event = EventSerializer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = Notification
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "pk",
 | 
				
			||||||
 | 
					            "severity",
 | 
				
			||||||
 | 
					            "body",
 | 
				
			||||||
 | 
					            "created",
 | 
				
			||||||
 | 
					            "event",
 | 
				
			||||||
 | 
					            "seen",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationViewSet(
 | 
				
			||||||
 | 
					    mixins.RetrieveModelMixin,
 | 
				
			||||||
 | 
					    mixins.UpdateModelMixin,
 | 
				
			||||||
 | 
					    mixins.DestroyModelMixin,
 | 
				
			||||||
 | 
					    mixins.ListModelMixin,
 | 
				
			||||||
 | 
					    GenericViewSet,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Notification Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = Notification.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = NotificationSerializer
 | 
				
			||||||
 | 
					    filterset_fields = [
 | 
				
			||||||
 | 
					        "severity",
 | 
				
			||||||
 | 
					        "body",
 | 
				
			||||||
 | 
					        "created",
 | 
				
			||||||
 | 
					        "event",
 | 
				
			||||||
 | 
					        "seen",
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
					        if not self.request:
 | 
				
			||||||
 | 
					            return super().get_queryset()
 | 
				
			||||||
 | 
					        return Notification.objects.filter(user=self.request.user)
 | 
				
			||||||
							
								
								
									
										28
									
								
								authentik/events/api/notification_rule.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								authentik/events/api/notification_rule.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					"""NotificationRule API Views"""
 | 
				
			||||||
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import NotificationRule
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRuleSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    """NotificationRule Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = NotificationRule
 | 
				
			||||||
 | 
					        depth = 2
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "pk",
 | 
				
			||||||
 | 
					            "name",
 | 
				
			||||||
 | 
					            "transports",
 | 
				
			||||||
 | 
					            "severity",
 | 
				
			||||||
 | 
					            "group",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRuleViewSet(ModelViewSet):
 | 
				
			||||||
 | 
					    """NotificationRule Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = NotificationRule.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = NotificationRuleSerializer
 | 
				
			||||||
							
								
								
									
										66
									
								
								authentik/events/api/notification_transport.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								authentik/events/api/notification_transport.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					"""NotificationTransport API Views"""
 | 
				
			||||||
 | 
					from django.http.response import Http404
 | 
				
			||||||
 | 
					from guardian.shortcuts import get_objects_for_user
 | 
				
			||||||
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
 | 
					from rest_framework.request import Request
 | 
				
			||||||
 | 
					from rest_framework.response import Response
 | 
				
			||||||
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import (
 | 
				
			||||||
 | 
					    Notification,
 | 
				
			||||||
 | 
					    NotificationSeverity,
 | 
				
			||||||
 | 
					    NotificationTransport,
 | 
				
			||||||
 | 
					    NotificationTransportError,
 | 
				
			||||||
 | 
					    TransportMode,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    """NotificationTransport Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mode_verbose = SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_mode_verbose(self, instance: NotificationTransport):
 | 
				
			||||||
 | 
					        """Return selected mode with a UI Label"""
 | 
				
			||||||
 | 
					        return TransportMode(instance.mode).label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = NotificationTransport
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "pk",
 | 
				
			||||||
 | 
					            "name",
 | 
				
			||||||
 | 
					            "mode",
 | 
				
			||||||
 | 
					            "mode_verbose",
 | 
				
			||||||
 | 
					            "webhook_url",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportViewSet(ModelViewSet):
 | 
				
			||||||
 | 
					    """NotificationTransport Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = NotificationTransport.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = NotificationTransportSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action(detail=True, methods=["post"])
 | 
				
			||||||
 | 
					    # pylint: disable=invalid-name
 | 
				
			||||||
 | 
					    def test(self, request: Request, pk=None) -> Response:
 | 
				
			||||||
 | 
					        """Send example notification using selected transport. Requires
 | 
				
			||||||
 | 
					        Modify permissions."""
 | 
				
			||||||
 | 
					        transports = get_objects_for_user(
 | 
				
			||||||
 | 
					            request.user, "authentik_events.change_notificationtransport"
 | 
				
			||||||
 | 
					        ).filter(pk=pk)
 | 
				
			||||||
 | 
					        if not transports.exists():
 | 
				
			||||||
 | 
					            raise Http404
 | 
				
			||||||
 | 
					        transport: NotificationTransport = transports.first()
 | 
				
			||||||
 | 
					        notification = Notification(
 | 
				
			||||||
 | 
					            severity=NotificationSeverity.NOTICE,
 | 
				
			||||||
 | 
					            body=f"Test Notification from transport {transport.name}",
 | 
				
			||||||
 | 
					            user=request.user,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return Response(transport.send(notification))
 | 
				
			||||||
 | 
					        except NotificationTransportError as exc:
 | 
				
			||||||
 | 
					            return Response(str(exc.__cause__ or None), status=503)
 | 
				
			||||||
							
								
								
									
										47
									
								
								authentik/events/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								authentik/events/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					"""authentik events NotificationTransport forms"""
 | 
				
			||||||
 | 
					from django import forms
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import NotificationRule, NotificationTransport
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    """NotificationTransport Form"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = NotificationTransport
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "name",
 | 
				
			||||||
 | 
					            "mode",
 | 
				
			||||||
 | 
					            "webhook_url",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
 | 
					            "webhook_url": forms.TextInput(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        labels = {
 | 
				
			||||||
 | 
					            "webhook_url": _("Webhook URL"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        help_texts = {
 | 
				
			||||||
 | 
					            "webhook_url": _(
 | 
				
			||||||
 | 
					                ("Only required when the Generic or Slack Webhook is used.")
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRuleForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    """NotificationRule Form"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = NotificationRule
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "name",
 | 
				
			||||||
 | 
					            "group",
 | 
				
			||||||
 | 
					            "transports",
 | 
				
			||||||
 | 
					            "severity",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
@ -6,9 +6,10 @@ from django.contrib.auth.models import User
 | 
				
			|||||||
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
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
 | 
					from guardian.models import UserObjectPermission
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.middleware import LOCAL
 | 
					from authentik.core.middleware import LOCAL
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction, Notification
 | 
				
			||||||
from authentik.events.signals import EventNewThread
 | 
					from authentik.events.signals import EventNewThread
 | 
				
			||||||
from authentik.events.utils import model_to_dict
 | 
					from authentik.events.utils import model_to_dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -63,7 +64,7 @@ class AuditMiddleware:
 | 
				
			|||||||
        user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
 | 
					        user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        """Signal handler for all object's post_save"""
 | 
					        """Signal handler for all object's post_save"""
 | 
				
			||||||
        if isinstance(instance, Event):
 | 
					        if isinstance(instance, (Event, Notification, UserObjectPermission)):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
 | 
					        action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
 | 
				
			||||||
@ -75,7 +76,7 @@ class AuditMiddleware:
 | 
				
			|||||||
        user: User, request: HttpRequest, sender, instance: Model, **_
 | 
					        user: User, request: HttpRequest, sender, instance: Model, **_
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        """Signal handler for all object's pre_delete"""
 | 
					        """Signal handler for all object's pre_delete"""
 | 
				
			||||||
        if isinstance(instance, Event):
 | 
					        if isinstance(instance, (Event, Notification, UserObjectPermission)):
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        EventNewThread(
 | 
					        EventNewThread(
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,148 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-11 16:36
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
 | 
					        ("authentik_policies", "0004_policy_execution_logging"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0016_auto_20201202_2234"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0009_auto_20201227_1210"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="NotificationTransport",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4,
 | 
				
			||||||
 | 
					                        editable=False,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField(unique=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "mode",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("webhook", "Generic Webhook"),
 | 
				
			||||||
 | 
					                            ("webhook_slack", "Slack Webhook (Slack/Discord)"),
 | 
				
			||||||
 | 
					                            ("email", "Email"),
 | 
				
			||||||
 | 
					                        ]
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("webhook_url", models.TextField(blank=True)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification Transport",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notification Transports",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="NotificationRule",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "policybindingmodel_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_policies.policybindingmodel",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField(unique=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "severity",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("notice", "Notice"),
 | 
				
			||||||
 | 
					                            ("warning", "Warning"),
 | 
				
			||||||
 | 
					                            ("alert", "Alert"),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        default="notice",
 | 
				
			||||||
 | 
					                        help_text="Controls which severity level the created notifications will have.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "group",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
 | 
				
			||||||
 | 
					                        null=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                        to="authentik_core.group",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "transports",
 | 
				
			||||||
 | 
					                    models.ManyToManyField(
 | 
				
			||||||
 | 
					                        help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
 | 
				
			||||||
 | 
					                        to="authentik_events.NotificationTransport",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification Rule",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notification Rules",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_policies.policybindingmodel",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="Notification",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4,
 | 
				
			||||||
 | 
					                        editable=False,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "severity",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("notice", "Notice"),
 | 
				
			||||||
 | 
					                            ("warning", "Warning"),
 | 
				
			||||||
 | 
					                            ("alert", "Alert"),
 | 
				
			||||||
 | 
					                        ]
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("body", models.TextField()),
 | 
				
			||||||
 | 
					                ("created", models.DateTimeField(auto_now_add=True)),
 | 
				
			||||||
 | 
					                ("seen", models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "event",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        null=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                        to="authentik_events.event",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "user",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        to=settings.AUTH_USER_MODEL,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notifications",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-10 18:57
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import EventAction, NotificationSeverity, TransportMode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
 | 
				
			||||||
 | 
					    EventMatcherPolicy = apps.get_model(
 | 
				
			||||||
 | 
					        "authentik_policies_event_matcher", "EventMatcherPolicy"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    NotificationRule = apps.get_model("authentik_events", "NotificationRule")
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    admin_group = (
 | 
				
			||||||
 | 
					        Group.objects.using(db_alias)
 | 
				
			||||||
 | 
					        .filter(name="authentik Admins", is_superuser=True)
 | 
				
			||||||
 | 
					        .first()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-configuration-error",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.CONFIGURATION_ERROR},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-notify-configuration-error",
 | 
				
			||||||
 | 
					        defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.transports.set(
 | 
				
			||||||
 | 
					        NotificationTransport.objects.using(db_alias).filter(
 | 
				
			||||||
 | 
					            name="default-email-transport"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.save()
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 0,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
 | 
				
			||||||
 | 
					    EventMatcherPolicy = apps.get_model(
 | 
				
			||||||
 | 
					        "authentik_policies_event_matcher", "EventMatcherPolicy"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    NotificationRule = apps.get_model("authentik_events", "NotificationRule")
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    admin_group = (
 | 
				
			||||||
 | 
					        Group.objects.using(db_alias)
 | 
				
			||||||
 | 
					        .filter(name="authentik Admins", is_superuser=True)
 | 
				
			||||||
 | 
					        .first()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-update",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.UPDATE_AVAILABLE},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-notify-update",
 | 
				
			||||||
 | 
					        defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.transports.set(
 | 
				
			||||||
 | 
					        NotificationTransport.objects.using(db_alias).filter(
 | 
				
			||||||
 | 
					            name="default-email-transport"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.save()
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 0,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
 | 
				
			||||||
 | 
					    EventMatcherPolicy = apps.get_model(
 | 
				
			||||||
 | 
					        "authentik_policies_event_matcher", "EventMatcherPolicy"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    NotificationRule = apps.get_model("authentik_events", "NotificationRule")
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    admin_group = (
 | 
				
			||||||
 | 
					        Group.objects.using(db_alias)
 | 
				
			||||||
 | 
					        .filter(name="authentik Admins", is_superuser=True)
 | 
				
			||||||
 | 
					        .first()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-policy-exception",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.POLICY_EXCEPTION},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-property-mapping-exception",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-notify-exception",
 | 
				
			||||||
 | 
					        defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.transports.set(
 | 
				
			||||||
 | 
					        NotificationTransport.objects.using(db_alias).filter(
 | 
				
			||||||
 | 
					            name="default-email-transport"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.save()
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy_policy_exc,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 0,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy_pm_exc,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 1,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    NotificationTransport.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-email-transport",
 | 
				
			||||||
 | 
					        defaults={"mode": TransportMode.EMAIL},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            "authentik_events",
 | 
				
			||||||
 | 
					            "0010_notification_notificationtransport_notificationrule",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        ("authentik_core", "0016_auto_20201202_2234"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
 | 
				
			||||||
 | 
					        ("authentik_policies", "0004_policy_execution_logging"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RunPython(transport_email_global),
 | 
				
			||||||
 | 
					        migrations.RunPython(notify_configuration_error),
 | 
				
			||||||
 | 
					        migrations.RunPython(notify_update),
 | 
				
			||||||
 | 
					        migrations.RunPython(notify_exception),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
"""authentik events models"""
 | 
					"""authentik events models"""
 | 
				
			||||||
 | 
					 | 
				
			||||||
from inspect import getmodule, stack
 | 
					from inspect import getmodule, stack
 | 
				
			||||||
 | 
					from smtplib import SMTPException
 | 
				
			||||||
from typing import Optional, Union
 | 
					from typing import Optional, Union
 | 
				
			||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -9,19 +9,28 @@ from django.core.exceptions import ValidationError
 | 
				
			|||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from structlog import get_logger
 | 
					from requests import RequestException, post
 | 
				
			||||||
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.core.middleware import (
 | 
					from authentik.core.middleware import (
 | 
				
			||||||
    SESSION_IMPERSONATE_ORIGINAL_USER,
 | 
					    SESSION_IMPERSONATE_ORIGINAL_USER,
 | 
				
			||||||
    SESSION_IMPERSONATE_USER,
 | 
					    SESSION_IMPERSONATE_USER,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import Group, User
 | 
				
			||||||
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
 | 
					from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
 | 
				
			||||||
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
 | 
					from authentik.policies.models import PolicyBindingModel
 | 
				
			||||||
 | 
					from authentik.stages.email.utils import TemplateEmailMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger("authentik.events")
 | 
					LOGGER = get_logger("authentik.events")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransportError(SentryIgnoredException):
 | 
				
			||||||
 | 
					    """Error raised when a notification fails to be delivered"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EventAction(models.TextChoices):
 | 
					class EventAction(models.TextChoices):
 | 
				
			||||||
    """All possible actions to save into the events log"""
 | 
					    """All possible actions to save into the events log"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -47,6 +56,9 @@ class EventAction(models.TextChoices):
 | 
				
			|||||||
    POLICY_EXCEPTION = "policy_exception"
 | 
					    POLICY_EXCEPTION = "policy_exception"
 | 
				
			||||||
    PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
 | 
					    PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SYSTEM_TASK_EXECUTION = "system_task_execution"
 | 
				
			||||||
 | 
					    SYSTEM_TASK_EXCEPTION = "system_task_exception"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    CONFIGURATION_ERROR = "configuration_error"
 | 
					    CONFIGURATION_ERROR = "configuration_error"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MODEL_CREATED = "model_created"
 | 
					    MODEL_CREATED = "model_created"
 | 
				
			||||||
@ -104,10 +116,12 @@ class Event(models.Model):
 | 
				
			|||||||
        Events independently from requests.
 | 
					        Events independently from requests.
 | 
				
			||||||
        `user` arguments optionally overrides user from requests."""
 | 
					        `user` arguments optionally overrides user from requests."""
 | 
				
			||||||
        if hasattr(request, "user"):
 | 
					        if hasattr(request, "user"):
 | 
				
			||||||
            self.user = get_user(
 | 
					            original_user = None
 | 
				
			||||||
                request.user,
 | 
					            if hasattr(request, "session"):
 | 
				
			||||||
                request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None),
 | 
					                original_user = request.session.get(
 | 
				
			||||||
            )
 | 
					                    SESSION_IMPERSONATE_ORIGINAL_USER, None
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            self.user = get_user(request.user, original_user)
 | 
				
			||||||
        if user:
 | 
					        if user:
 | 
				
			||||||
            self.user = get_user(user)
 | 
					            self.user = get_user(user)
 | 
				
			||||||
        # Check if we're currently impersonating, and add that user
 | 
					        # Check if we're currently impersonating, and add that user
 | 
				
			||||||
@ -127,9 +141,7 @@ class Event(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        if not self._state.adding:
 | 
					        if not self._state.adding:
 | 
				
			||||||
            raise ValidationError(
 | 
					            raise ValidationError("you may not edit an existing Event")
 | 
				
			||||||
                "you may not edit an existing %s" % self._meta.model_name
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Created Event",
 | 
					            "Created Event",
 | 
				
			||||||
            action=self.action,
 | 
					            action=self.action,
 | 
				
			||||||
@ -139,7 +151,219 @@ class Event(models.Model):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        return super().save(*args, **kwargs)
 | 
					        return super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def summary(self) -> str:
 | 
				
			||||||
 | 
					        """Return a summary of this event."""
 | 
				
			||||||
 | 
					        if "message" in self.context:
 | 
				
			||||||
 | 
					            return self.context["message"]
 | 
				
			||||||
 | 
					        return f"{self.action}: {self.context}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        return f"<Event action={self.action} user={self.user} context={self.context}>"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Event")
 | 
					        verbose_name = _("Event")
 | 
				
			||||||
        verbose_name_plural = _("Events")
 | 
					        verbose_name_plural = _("Events")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TransportMode(models.TextChoices):
 | 
				
			||||||
 | 
					    """Modes that a notification transport can send a notification"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    WEBHOOK = "webhook", _("Generic Webhook")
 | 
				
			||||||
 | 
					    WEBHOOK_SLACK = "webhook_slack", _("Slack Webhook (Slack/Discord)")
 | 
				
			||||||
 | 
					    EMAIL = "email", _("Email")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationTransport(models.Model):
 | 
				
			||||||
 | 
					    """Action which is executed when a Rule matches"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name = models.TextField(unique=True)
 | 
				
			||||||
 | 
					    mode = models.TextField(choices=TransportMode.choices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    webhook_url = models.TextField(blank=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def send(self, notification: "Notification") -> list[str]:
 | 
				
			||||||
 | 
					        """Send notification to user, called from async task"""
 | 
				
			||||||
 | 
					        if self.mode == TransportMode.WEBHOOK:
 | 
				
			||||||
 | 
					            return self.send_webhook(notification)
 | 
				
			||||||
 | 
					        if self.mode == TransportMode.WEBHOOK_SLACK:
 | 
				
			||||||
 | 
					            return self.send_webhook_slack(notification)
 | 
				
			||||||
 | 
					        if self.mode == TransportMode.EMAIL:
 | 
				
			||||||
 | 
					            return self.send_email(notification)
 | 
				
			||||||
 | 
					        raise ValueError(f"Invalid mode {self.mode} set")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def send_webhook(self, notification: "Notification") -> list[str]:
 | 
				
			||||||
 | 
					        """Send notification to generic webhook"""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            response = post(
 | 
				
			||||||
 | 
					                self.webhook_url,
 | 
				
			||||||
 | 
					                json={
 | 
				
			||||||
 | 
					                    "body": notification.body,
 | 
				
			||||||
 | 
					                    "severity": notification.severity,
 | 
				
			||||||
 | 
					                    "user_email": notification.user.email,
 | 
				
			||||||
 | 
					                    "user_username": notification.user.username,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            response.raise_for_status()
 | 
				
			||||||
 | 
					        except RequestException as exc:
 | 
				
			||||||
 | 
					            raise NotificationTransportError(exc.response.text) from exc
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            response.status_code,
 | 
				
			||||||
 | 
					            response.text,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def send_webhook_slack(self, notification: "Notification") -> list[str]:
 | 
				
			||||||
 | 
					        """Send notification to slack or slack-compatible endpoints"""
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "title": _("Severity"),
 | 
				
			||||||
 | 
					                "value": notification.severity,
 | 
				
			||||||
 | 
					                "short": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "title": _("Dispatched for user"),
 | 
				
			||||||
 | 
					                "value": str(notification.user),
 | 
				
			||||||
 | 
					                "short": True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        if notification.event:
 | 
				
			||||||
 | 
					            for key, value in notification.event.context.items():
 | 
				
			||||||
 | 
					                if not isinstance(value, str):
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                # https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html
 | 
				
			||||||
 | 
					                if len(fields) >= 25:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                fields.append({"title": key[:256], "value": value[:1024]})
 | 
				
			||||||
 | 
					        body = {
 | 
				
			||||||
 | 
					            "username": "authentik",
 | 
				
			||||||
 | 
					            "icon_url": "https://goauthentik.io/img/icon.png",
 | 
				
			||||||
 | 
					            "attachments": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "author_name": "authentik",
 | 
				
			||||||
 | 
					                    "author_link": "https://goauthentik.io",
 | 
				
			||||||
 | 
					                    "author_icon": "https://goauthentik.io/img/icon.png",
 | 
				
			||||||
 | 
					                    "title": notification.body,
 | 
				
			||||||
 | 
					                    "color": "#fd4b2d",
 | 
				
			||||||
 | 
					                    "fields": fields,
 | 
				
			||||||
 | 
					                    "footer": f"authentik v{__version__}",
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if notification.event:
 | 
				
			||||||
 | 
					            body["attachments"][0]["title"] = notification.event.action
 | 
				
			||||||
 | 
					            body["attachments"][0]["text"] = notification.event.action
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            response = post(self.webhook_url, json=body)
 | 
				
			||||||
 | 
					            response.raise_for_status()
 | 
				
			||||||
 | 
					        except RequestException as exc:
 | 
				
			||||||
 | 
					            raise NotificationTransportError(exc.response.text) from exc
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            response.status_code,
 | 
				
			||||||
 | 
					            response.text,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def send_email(self, notification: "Notification") -> list[str]:
 | 
				
			||||||
 | 
					        """Send notification via global email configuration"""
 | 
				
			||||||
 | 
					        body_trunc = (
 | 
				
			||||||
 | 
					            (notification.body[:75] + "..")
 | 
				
			||||||
 | 
					            if len(notification.body) > 75
 | 
				
			||||||
 | 
					            else notification.body
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        mail = TemplateEmailMessage(
 | 
				
			||||||
 | 
					            subject=f"authentik Notification: {body_trunc}",
 | 
				
			||||||
 | 
					            template_name="email/setup.html",
 | 
				
			||||||
 | 
					            to=[notification.user.email],
 | 
				
			||||||
 | 
					            template_context={
 | 
				
			||||||
 | 
					                "body": notification.body,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        # Email is sent directly here, as the call to send() should have been from a task.
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            from authentik.stages.email.tasks import send_mail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # pyright: reportGeneralTypeIssues=false
 | 
				
			||||||
 | 
					            return send_mail(mail.__dict__)  # pylint: disable=no-value-for-parameter
 | 
				
			||||||
 | 
					        except (SMTPException, ConnectionError, OSError) as exc:
 | 
				
			||||||
 | 
					            raise NotificationTransportError from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        return f"Notification Transport {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        verbose_name = _("Notification Transport")
 | 
				
			||||||
 | 
					        verbose_name_plural = _("Notification Transports")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationSeverity(models.TextChoices):
 | 
				
			||||||
 | 
					    """Severity images that a notification can have"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    NOTICE = "notice", _("Notice")
 | 
				
			||||||
 | 
					    WARNING = "warning", _("Warning")
 | 
				
			||||||
 | 
					    ALERT = "alert", _("Alert")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Notification(models.Model):
 | 
				
			||||||
 | 
					    """Event Notification"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
				
			||||||
 | 
					    severity = models.TextField(choices=NotificationSeverity.choices)
 | 
				
			||||||
 | 
					    body = models.TextField()
 | 
				
			||||||
 | 
					    created = models.DateTimeField(auto_now_add=True)
 | 
				
			||||||
 | 
					    event = models.ForeignKey(Event, on_delete=models.SET_NULL, null=True, blank=True)
 | 
				
			||||||
 | 
					    seen = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    user = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        body_trunc = (self.body[:75] + "..") if len(self.body) > 75 else self.body
 | 
				
			||||||
 | 
					        return f"Notification for user {self.user}: {body_trunc}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        verbose_name = _("Notification")
 | 
				
			||||||
 | 
					        verbose_name_plural = _("Notifications")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotificationRule(PolicyBindingModel):
 | 
				
			||||||
 | 
					    """Decide when to create a Notification based on policies attached to this object."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name = models.TextField(unique=True)
 | 
				
			||||||
 | 
					    transports = models.ManyToManyField(
 | 
				
			||||||
 | 
					        NotificationTransport,
 | 
				
			||||||
 | 
					        help_text=_(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "Select which transports should be used to notify the user. If none are "
 | 
				
			||||||
 | 
					                "selected, the notification will only be shown in the authentik UI."
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    severity = models.TextField(
 | 
				
			||||||
 | 
					        choices=NotificationSeverity.choices,
 | 
				
			||||||
 | 
					        default=NotificationSeverity.NOTICE,
 | 
				
			||||||
 | 
					        help_text=_(
 | 
				
			||||||
 | 
					            "Controls which severity level the created notifications will have."
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    group = models.ForeignKey(
 | 
				
			||||||
 | 
					        Group,
 | 
				
			||||||
 | 
					        help_text=_(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "Define which group of users this notification should be sent and shown to. "
 | 
				
			||||||
 | 
					                "If left empty, Notification won't ben sent."
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        return f"Notification Rule {self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        verbose_name = _("Notification Rule")
 | 
				
			||||||
 | 
					        verbose_name_plural = _("Notification Rules")
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,8 @@ from typing import Any, Dict, List, Optional
 | 
				
			|||||||
from celery import Task
 | 
					from celery import Task
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TaskResultStatus(Enum):
 | 
					class TaskResultStatus(Enum):
 | 
				
			||||||
    """Possible states of tasks"""
 | 
					    """Possible states of tasks"""
 | 
				
			||||||
@ -52,6 +54,11 @@ class TaskInfo:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    task_description: Optional[str] = field(default=None)
 | 
					    task_description: Optional[str] = field(default=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def html_name(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Get task_name, but split on underscores, so we can join in the html template."""
 | 
				
			||||||
 | 
					        return self.task_name.split("_")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def all() -> Dict[str, "TaskInfo"]:
 | 
					    def all() -> Dict[str, "TaskInfo"]:
 | 
				
			||||||
        """Get all TaskInfo objects"""
 | 
					        """Get all TaskInfo objects"""
 | 
				
			||||||
@ -117,6 +124,13 @@ class MonitoredTask(Task):
 | 
				
			|||||||
                task_call_args=args,
 | 
					                task_call_args=args,
 | 
				
			||||||
                task_call_kwargs=kwargs,
 | 
					                task_call_kwargs=kwargs,
 | 
				
			||||||
            ).save(self.result_timeout_hours)
 | 
					            ).save(self.result_timeout_hours)
 | 
				
			||||||
 | 
					            Event.new(
 | 
				
			||||||
 | 
					                EventAction.SYSTEM_TASK_EXECUTION,
 | 
				
			||||||
 | 
					                message=(
 | 
				
			||||||
 | 
					                    f"Task {self.__name__} finished successfully: "
 | 
				
			||||||
 | 
					                    "\n".join(self._result.messages)
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ).save()
 | 
				
			||||||
        return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
 | 
					        return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=too-many-arguments
 | 
					    # pylint: disable=too-many-arguments
 | 
				
			||||||
@ -133,6 +147,13 @@ class MonitoredTask(Task):
 | 
				
			|||||||
            task_call_args=args,
 | 
					            task_call_args=args,
 | 
				
			||||||
            task_call_kwargs=kwargs,
 | 
					            task_call_kwargs=kwargs,
 | 
				
			||||||
        ).save(self.result_timeout_hours)
 | 
					        ).save(self.result_timeout_hours)
 | 
				
			||||||
 | 
					        Event.new(
 | 
				
			||||||
 | 
					            EventAction.SYSTEM_TASK_EXCEPTION,
 | 
				
			||||||
 | 
					            message=(
 | 
				
			||||||
 | 
					                f"Task {self.__name__} encountered an error: "
 | 
				
			||||||
 | 
					                "\n".join(self._result.messages)
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ).save()
 | 
				
			||||||
        return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
 | 
					        return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def run(self, *args, **kwargs):
 | 
					    def run(self, *args, **kwargs):
 | 
				
			||||||
@ -7,12 +7,16 @@ from django.contrib.auth.signals import (
 | 
				
			|||||||
    user_logged_out,
 | 
					    user_logged_out,
 | 
				
			||||||
    user_login_failed,
 | 
					    user_login_failed,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.core.signals import password_changed
 | 
					from authentik.core.signals import password_changed
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					from authentik.events.tasks import event_notification_handler
 | 
				
			||||||
 | 
					from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
 | 
				
			||||||
 | 
					from authentik.flows.views import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.stages.invitation.models import Invitation
 | 
					from authentik.stages.invitation.models import Invitation
 | 
				
			||||||
from authentik.stages.invitation.signals import invitation_used
 | 
					from authentik.stages.invitation.signals import invitation_used
 | 
				
			||||||
from authentik.stages.user_write.signals import user_write
 | 
					from authentik.stages.user_write.signals import user_write
 | 
				
			||||||
@ -44,6 +48,11 @@ class EventNewThread(Thread):
 | 
				
			|||||||
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
 | 
					def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
 | 
				
			||||||
    """Log successful login"""
 | 
					    """Log successful login"""
 | 
				
			||||||
    thread = EventNewThread(EventAction.LOGIN, request)
 | 
					    thread = EventNewThread(EventAction.LOGIN, request)
 | 
				
			||||||
 | 
					    if SESSION_KEY_PLAN in request.session:
 | 
				
			||||||
 | 
					        flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
 | 
					        if PLAN_CONTEXT_SOURCE in flow_plan.context:
 | 
				
			||||||
 | 
					            # Login request came from an external source, save it in the context
 | 
				
			||||||
 | 
					            thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
 | 
				
			||||||
    thread.user = user
 | 
					    thread.user = user
 | 
				
			||||||
    thread.run()
 | 
					    thread.run()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -95,3 +104,10 @@ def on_password_changed(sender, user: User, password: str, **_):
 | 
				
			|||||||
    """Log password change"""
 | 
					    """Log password change"""
 | 
				
			||||||
    thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user)
 | 
					    thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user)
 | 
				
			||||||
    thread.run()
 | 
					    thread.run()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@receiver(post_save, sender=Event)
 | 
				
			||||||
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
 | 
					def event_post_save_notification(sender, instance: Event, **_):
 | 
				
			||||||
 | 
					    """Start task to check if any policies trigger an notification on this event"""
 | 
				
			||||||
 | 
					    event_notification_handler.delay(instance.event_uuid.hex)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										99
									
								
								authentik/events/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								authentik/events/tasks.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					"""Event notification tasks"""
 | 
				
			||||||
 | 
					from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import (
 | 
				
			||||||
 | 
					    Event,
 | 
				
			||||||
 | 
					    Notification,
 | 
				
			||||||
 | 
					    NotificationRule,
 | 
				
			||||||
 | 
					    NotificationTransport,
 | 
				
			||||||
 | 
					    NotificationTransportError,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
				
			||||||
 | 
					from authentik.policies.engine import PolicyEngine, PolicyEngineMode
 | 
				
			||||||
 | 
					from authentik.policies.models import PolicyBinding
 | 
				
			||||||
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CELERY_APP.task()
 | 
				
			||||||
 | 
					def event_notification_handler(event_uuid: str):
 | 
				
			||||||
 | 
					    """Start task for each trigger definition"""
 | 
				
			||||||
 | 
					    for trigger in NotificationRule.objects.all():
 | 
				
			||||||
 | 
					        event_trigger_handler.apply_async(
 | 
				
			||||||
 | 
					            args=[event_uuid, trigger.name], queue="authentik_events"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CELERY_APP.task()
 | 
				
			||||||
 | 
					def event_trigger_handler(event_uuid: str, trigger_name: str):
 | 
				
			||||||
 | 
					    """Check if policies attached to NotificationRule match event"""
 | 
				
			||||||
 | 
					    event: Event = Event.objects.get(event_uuid=event_uuid)
 | 
				
			||||||
 | 
					    trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if "policy_uuid" in event.context:
 | 
				
			||||||
 | 
					        policy_uuid = event.context["policy_uuid"]
 | 
				
			||||||
 | 
					        if PolicyBinding.objects.filter(
 | 
				
			||||||
 | 
					            target__in=NotificationRule.objects.all().values_list(
 | 
				
			||||||
 | 
					                "pbm_uuid", flat=True
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            policy=policy_uuid,
 | 
				
			||||||
 | 
					        ).exists():
 | 
				
			||||||
 | 
					            # If policy that caused this event to be created is attached
 | 
				
			||||||
 | 
					            # to *any* NotificationRule, we return early.
 | 
				
			||||||
 | 
					            # This is the most effective way to prevent infinite loops.
 | 
				
			||||||
 | 
					            LOGGER.debug(
 | 
				
			||||||
 | 
					                "e(trigger): attempting to prevent infinite loop", trigger=trigger
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not trigger.group:
 | 
				
			||||||
 | 
					        LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
 | 
				
			||||||
 | 
					    policy_engine = PolicyEngine(trigger, get_anonymous_user())
 | 
				
			||||||
 | 
					    policy_engine.mode = PolicyEngineMode.MODE_OR
 | 
				
			||||||
 | 
					    policy_engine.empty_result = False
 | 
				
			||||||
 | 
					    policy_engine.use_cache = False
 | 
				
			||||||
 | 
					    policy_engine.request.context["event"] = event
 | 
				
			||||||
 | 
					    policy_engine.build()
 | 
				
			||||||
 | 
					    result = policy_engine.result
 | 
				
			||||||
 | 
					    if not result.passing:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
 | 
				
			||||||
 | 
					    # Create the notification objects
 | 
				
			||||||
 | 
					    for user in trigger.group.users.all():
 | 
				
			||||||
 | 
					        notification = Notification.objects.create(
 | 
				
			||||||
 | 
					            severity=trigger.severity, body=event.summary, event=event, user=user
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for transport in trigger.transports.all():
 | 
				
			||||||
 | 
					            notification_transport.apply_async(
 | 
				
			||||||
 | 
					                args=[notification.pk, transport.pk], queue="authentik_events"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CELERY_APP.task(
 | 
				
			||||||
 | 
					    bind=True,
 | 
				
			||||||
 | 
					    autoretry_for=(NotificationTransportError,),
 | 
				
			||||||
 | 
					    retry_backoff=True,
 | 
				
			||||||
 | 
					    base=MonitoredTask,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def notification_transport(
 | 
				
			||||||
 | 
					    self: MonitoredTask, notification_pk: int, transport_pk: int
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """Send notification over specified transport"""
 | 
				
			||||||
 | 
					    self.save_on_success = False
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        notification: Notification = Notification.objects.get(pk=notification_pk)
 | 
				
			||||||
 | 
					        transport: NotificationTransport = NotificationTransport.objects.get(
 | 
				
			||||||
 | 
					            pk=transport_pk
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        transport.send(notification)
 | 
				
			||||||
 | 
					        self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
 | 
				
			||||||
 | 
					    except NotificationTransportError as exc:
 | 
				
			||||||
 | 
					        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
 | 
				
			||||||
 | 
					        raise exc
 | 
				
			||||||
							
								
								
									
										24
									
								
								authentik/events/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								authentik/events/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					"""Event API tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.shortcuts import reverse
 | 
				
			||||||
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import User
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestEventsAPI(APITestCase):
 | 
				
			||||||
 | 
					    """Test Event API"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_top_n(self):
 | 
				
			||||||
 | 
					        """Test top_per_user"""
 | 
				
			||||||
 | 
					        user = User.objects.get(username="akadmin")
 | 
				
			||||||
 | 
					        self.client.force_login(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        event = Event.new(EventAction.AUTHORIZE_APPLICATION)
 | 
				
			||||||
 | 
					        event.save()  # We save to ensure nothing is un-saveable
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:event-top-per-user"),
 | 
				
			||||||
 | 
					            data={"filter_action": EventAction.AUTHORIZE_APPLICATION},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
"""events event tests"""
 | 
					"""event tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
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 authentik.core.models import Group
 | 
				
			||||||
from authentik.events.models import Event
 | 
					from authentik.events.models import Event
 | 
				
			||||||
from authentik.policies.dummy.models import DummyPolicy
 | 
					from authentik.policies.dummy.models import DummyPolicy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,14 +14,24 @@ class TestEvents(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def test_new_with_model(self):
 | 
					    def test_new_with_model(self):
 | 
				
			||||||
        """Create a new Event passing a model as kwarg"""
 | 
					        """Create a new Event passing a model as kwarg"""
 | 
				
			||||||
        event = Event.new("unittest", test={"model": get_anonymous_user()})
 | 
					        test_model = Group.objects.create(name="test")
 | 
				
			||||||
 | 
					        event = Event.new("unittest", test={"model": test_model})
 | 
				
			||||||
        event.save()  # We save to ensure nothing is un-saveable
 | 
					        event.save()  # We save to ensure nothing is un-saveable
 | 
				
			||||||
        model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
 | 
					        model_content_type = ContentType.objects.get_for_model(test_model)
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            event.context.get("test").get("model").get("app"),
 | 
					            event.context.get("test").get("model").get("app"),
 | 
				
			||||||
            model_content_type.app_label,
 | 
					            model_content_type.app_label,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_new_with_user(self):
 | 
				
			||||||
 | 
					        """Create a new Event passing a user as kwarg"""
 | 
				
			||||||
 | 
					        event = Event.new("unittest", test={"model": get_anonymous_user()})
 | 
				
			||||||
 | 
					        event.save()  # We save to ensure nothing is un-saveable
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            event.context.get("test").get("model").get("username"),
 | 
				
			||||||
 | 
					            get_anonymous_user().username,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_new_with_uuid_model(self):
 | 
					    def test_new_with_uuid_model(self):
 | 
				
			||||||
        """Create a new Event passing a model (with UUID PK) as kwarg"""
 | 
					        """Create a new Event passing a model (with UUID PK) as kwarg"""
 | 
				
			||||||
        temp_model = DummyPolicy.objects.create(name="test", result=True)
 | 
					        temp_model = DummyPolicy.objects.create(name="test", result=True)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										48
									
								
								authentik/events/tests/test_middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								authentik/events/tests/test_middleware.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					"""Event Middleware tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.shortcuts import reverse
 | 
				
			||||||
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import Application, User
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestEventsMiddleware(APITestCase):
 | 
				
			||||||
 | 
					    """Test Event Middleware"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
 | 
					        super().setUp()
 | 
				
			||||||
 | 
					        self.user = User.objects.get(username="akadmin")
 | 
				
			||||||
 | 
					        self.client.force_login(self.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_create(self):
 | 
				
			||||||
 | 
					        """Test model creation event"""
 | 
				
			||||||
 | 
					        self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_api:application-list"),
 | 
				
			||||||
 | 
					            data={"name": "test-create", "slug": "test-create"},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertTrue(Application.objects.filter(name="test-create").exists())
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.MODEL_CREATED,
 | 
				
			||||||
 | 
					                context__model__model_name="application",
 | 
				
			||||||
 | 
					                context__model__app="authentik_core",
 | 
				
			||||||
 | 
					                context__model__name="test-create",
 | 
				
			||||||
 | 
					            ).exists()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_delete(self):
 | 
				
			||||||
 | 
					        """Test model creation event"""
 | 
				
			||||||
 | 
					        Application.objects.create(name="test-delete", slug="test-delete")
 | 
				
			||||||
 | 
					        self.client.delete(
 | 
				
			||||||
 | 
					            reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"})
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertFalse(Application.objects.filter(name="test").exists())
 | 
				
			||||||
 | 
					        self.assertTrue(
 | 
				
			||||||
 | 
					            Event.objects.filter(
 | 
				
			||||||
 | 
					                action=EventAction.MODEL_DELETED,
 | 
				
			||||||
 | 
					                context__model__model_name="application",
 | 
				
			||||||
 | 
					                context__model__app="authentik_core",
 | 
				
			||||||
 | 
					                context__model__name="test-delete",
 | 
				
			||||||
 | 
					            ).exists()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
							
								
								
									
										90
									
								
								authentik/events/tests/test_notifications.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								authentik/events/tests/test_notifications.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					"""Notification tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from unittest.mock import MagicMock, patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import Group, User
 | 
				
			||||||
 | 
					from authentik.events.models import (
 | 
				
			||||||
 | 
					    Event,
 | 
				
			||||||
 | 
					    EventAction,
 | 
				
			||||||
 | 
					    NotificationRule,
 | 
				
			||||||
 | 
					    NotificationTransport,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from authentik.policies.event_matcher.models import EventMatcherPolicy
 | 
				
			||||||
 | 
					from authentik.policies.exceptions import PolicyException
 | 
				
			||||||
 | 
					from authentik.policies.models import PolicyBinding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestEventsNotifications(TestCase):
 | 
				
			||||||
 | 
					    """Test Event Notifications"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self) -> None:
 | 
				
			||||||
 | 
					        self.group = Group.objects.create(name="test-group")
 | 
				
			||||||
 | 
					        self.user = User.objects.create(name="test-user")
 | 
				
			||||||
 | 
					        self.group.users.add(self.user)
 | 
				
			||||||
 | 
					        self.group.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_trigger_empty(self):
 | 
				
			||||||
 | 
					        """Test trigger without any policies attached"""
 | 
				
			||||||
 | 
					        transport = NotificationTransport.objects.create(name="transport")
 | 
				
			||||||
 | 
					        trigger = NotificationRule.objects.create(name="trigger", group=self.group)
 | 
				
			||||||
 | 
					        trigger.transports.add(transport)
 | 
				
			||||||
 | 
					        trigger.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        execute_mock = MagicMock()
 | 
				
			||||||
 | 
					        with patch("authentik.events.models.NotificationTransport.send", execute_mock):
 | 
				
			||||||
 | 
					            Event.new(EventAction.CUSTOM_PREFIX).save()
 | 
				
			||||||
 | 
					        self.assertEqual(execute_mock.call_count, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_trigger_single(self):
 | 
				
			||||||
 | 
					        """Test simple transport triggering"""
 | 
				
			||||||
 | 
					        transport = NotificationTransport.objects.create(name="transport")
 | 
				
			||||||
 | 
					        trigger = NotificationRule.objects.create(name="trigger", group=self.group)
 | 
				
			||||||
 | 
					        trigger.transports.add(transport)
 | 
				
			||||||
 | 
					        trigger.save()
 | 
				
			||||||
 | 
					        matcher = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            name="matcher", action=EventAction.CUSTOM_PREFIX
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        execute_mock = MagicMock()
 | 
				
			||||||
 | 
					        with patch("authentik.events.models.NotificationTransport.send", execute_mock):
 | 
				
			||||||
 | 
					            Event.new(EventAction.CUSTOM_PREFIX).save()
 | 
				
			||||||
 | 
					        self.assertEqual(execute_mock.call_count, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_trigger_no_group(self):
 | 
				
			||||||
 | 
					        """Test trigger without group"""
 | 
				
			||||||
 | 
					        trigger = NotificationRule.objects.create(name="trigger")
 | 
				
			||||||
 | 
					        matcher = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            name="matcher", action=EventAction.CUSTOM_PREFIX
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        execute_mock = MagicMock()
 | 
				
			||||||
 | 
					        with patch("authentik.events.models.NotificationTransport.send", execute_mock):
 | 
				
			||||||
 | 
					            Event.new(EventAction.CUSTOM_PREFIX).save()
 | 
				
			||||||
 | 
					        self.assertEqual(execute_mock.call_count, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_policy_error_recursive(self):
 | 
				
			||||||
 | 
					        """Test Policy error which would cause recursion"""
 | 
				
			||||||
 | 
					        transport = NotificationTransport.objects.create(name="transport")
 | 
				
			||||||
 | 
					        NotificationRule.objects.filter(name__startswith="default").delete()
 | 
				
			||||||
 | 
					        trigger = NotificationRule.objects.create(name="trigger", group=self.group)
 | 
				
			||||||
 | 
					        trigger.transports.add(transport)
 | 
				
			||||||
 | 
					        trigger.save()
 | 
				
			||||||
 | 
					        matcher = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            name="matcher", action=EventAction.CUSTOM_PREFIX
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        execute_mock = MagicMock()
 | 
				
			||||||
 | 
					        passes = MagicMock(side_effect=PolicyException)
 | 
				
			||||||
 | 
					        with patch(
 | 
				
			||||||
 | 
					            "authentik.policies.event_matcher.models.EventMatcherPolicy.passes", passes
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            with patch(
 | 
				
			||||||
 | 
					                "authentik.events.models.NotificationTransport.send", execute_mock
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                Event.new(EventAction.CUSTOM_PREFIX).save()
 | 
				
			||||||
 | 
					        self.assertEqual(passes.call_count, 1)
 | 
				
			||||||
@ -5,8 +5,10 @@ from typing import Any, Dict, Optional
 | 
				
			|||||||
from uuid import UUID
 | 
					from uuid import UUID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.models import AnonymousUser
 | 
					from django.contrib.auth.models import AnonymousUser
 | 
				
			||||||
 | 
					from django.core.handlers.wsgi import WSGIRequest
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db.models.base import Model
 | 
					from django.db.models.base import Model
 | 
				
			||||||
 | 
					from django.http.request import HttpRequest
 | 
				
			||||||
from django.views.debug import SafeExceptionReporterFilter
 | 
					from django.views.debug import SafeExceptionReporterFilter
 | 
				
			||||||
from guardian.utils import get_anonymous_user
 | 
					from guardian.utils import get_anonymous_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -83,10 +85,14 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
 | 
				
			|||||||
            value = asdict(value)
 | 
					            value = asdict(value)
 | 
				
			||||||
        if isinstance(value, dict):
 | 
					        if isinstance(value, dict):
 | 
				
			||||||
            final_dict[key] = sanitize_dict(value)
 | 
					            final_dict[key] = sanitize_dict(value)
 | 
				
			||||||
 | 
					        elif isinstance(value, User):
 | 
				
			||||||
 | 
					            final_dict[key] = sanitize_dict(get_user(value))
 | 
				
			||||||
        elif isinstance(value, models.Model):
 | 
					        elif isinstance(value, models.Model):
 | 
				
			||||||
            final_dict[key] = sanitize_dict(model_to_dict(value))
 | 
					            final_dict[key] = sanitize_dict(model_to_dict(value))
 | 
				
			||||||
        elif isinstance(value, UUID):
 | 
					        elif isinstance(value, UUID):
 | 
				
			||||||
            final_dict[key] = value.hex
 | 
					            final_dict[key] = value.hex
 | 
				
			||||||
 | 
					        elif isinstance(value, (HttpRequest, WSGIRequest)):
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            final_dict[key] = value
 | 
					            final_dict[key] = value
 | 
				
			||||||
    return final_dict
 | 
					    return final_dict
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from time import time
 | 
				
			|||||||
from django import db
 | 
					from django import db
 | 
				
			||||||
from django.core.management.base import BaseCommand
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
from django.test import RequestFactory
 | 
					from django.test import RequestFactory
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ from dataclasses import dataclass
 | 
				
			|||||||
from typing import TYPE_CHECKING, Optional
 | 
					from typing import TYPE_CHECKING, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http.request import HttpRequest
 | 
					from django.http.request import HttpRequest
 | 
				
			||||||
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.flows.models import Stage
 | 
					from authentik.flows.models import Stage
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ from django.http import HttpRequest
 | 
				
			|||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from model_utils.managers import InheritanceManager
 | 
					from model_utils.managers import InheritanceManager
 | 
				
			||||||
from rest_framework.serializers import BaseSerializer
 | 
					from rest_framework.serializers import BaseSerializer
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
 | 
					from authentik.lib.models import InheritanceForeignKey, SerializerModel
 | 
				
			||||||
from authentik.policies.models import PolicyBindingModel
 | 
					from authentik.policies.models import PolicyBindingModel
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ from django.core.cache import cache
 | 
				
			|||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from sentry_sdk.hub import Hub
 | 
					from sentry_sdk.hub import Hub
 | 
				
			||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
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 cleanse_dict
 | 
					from authentik.events.models import cleanse_dict
 | 
				
			||||||
@ -21,6 +21,7 @@ PLAN_CONTEXT_PENDING_USER = "pending_user"
 | 
				
			|||||||
PLAN_CONTEXT_SSO = "is_sso"
 | 
					PLAN_CONTEXT_SSO = "is_sso"
 | 
				
			||||||
PLAN_CONTEXT_REDIRECT = "redirect"
 | 
					PLAN_CONTEXT_REDIRECT = "redirect"
 | 
				
			||||||
PLAN_CONTEXT_APPLICATION = "application"
 | 
					PLAN_CONTEXT_APPLICATION = "application"
 | 
				
			||||||
 | 
					PLAN_CONTEXT_SOURCE = "source"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
 | 
					def cache_key(flow: Flow, user: Optional[User] = None) -> str:
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ from django.template.response import TemplateResponse
 | 
				
			|||||||
from django.utils.decorators import method_decorator
 | 
					from django.utils.decorators import method_decorator
 | 
				
			||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
 | 
					from django.views.decorators.clickjacking import xframe_options_sameorigin
 | 
				
			||||||
from django.views.generic import TemplateView, View
 | 
					from django.views.generic import TemplateView, View
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
 | 
					from authentik.core.models import USER_ATTRIBUTE_DEBUG
 | 
				
			||||||
from authentik.events.models import cleanse_dict
 | 
					from authentik.events.models import cleanse_dict
 | 
				
			||||||
 | 
				
			|||||||
@ -5,13 +5,15 @@ from contextlib import contextmanager
 | 
				
			|||||||
from glob import glob
 | 
					from glob import glob
 | 
				
			||||||
from json import dumps
 | 
					from json import dumps
 | 
				
			||||||
from time import time
 | 
					from time import time
 | 
				
			||||||
from typing import Any, Dict
 | 
					from typing import Any
 | 
				
			||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
from django.conf import ImproperlyConfigured
 | 
					from django.conf import ImproperlyConfigured
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik import __version__
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
 | 
					SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob(
 | 
				
			||||||
    "/etc/authentik/config.d/*.yml", recursive=True
 | 
					    "/etc/authentik/config.d/*.yml", recursive=True
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -19,10 +21,9 @@ ENV_PREFIX = "AUTHENTIK"
 | 
				
			|||||||
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
 | 
					ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def context_processor(request: HttpRequest) -> Dict[str, Any]:
 | 
					def context_processor(request: HttpRequest) -> dict[str, Any]:
 | 
				
			||||||
    """Context Processor that injects config object into every template"""
 | 
					    """Context Processor that injects config object into every template"""
 | 
				
			||||||
    kwargs = {"config": CONFIG.raw}
 | 
					    return {"config": CONFIG.raw, "ak_version": __version__}
 | 
				
			||||||
    return kwargs
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConfigLoader:
 | 
					class ConfigLoader:
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,17 @@ error_reporting:
 | 
				
			|||||||
  environment: customer
 | 
					  environment: customer
 | 
				
			||||||
  send_pii: false
 | 
					  send_pii: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Global email settings
 | 
				
			||||||
 | 
					email:
 | 
				
			||||||
 | 
					  host: localhost
 | 
				
			||||||
 | 
					  port: 25
 | 
				
			||||||
 | 
					  username: ""
 | 
				
			||||||
 | 
					  password: ""
 | 
				
			||||||
 | 
					  use_tls: false
 | 
				
			||||||
 | 
					  use_ssl: false
 | 
				
			||||||
 | 
					  timeout: 10
 | 
				
			||||||
 | 
					  from: authentik@localhost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
outposts:
 | 
					outposts:
 | 
				
			||||||
  docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
 | 
					  docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
 | 
				
			|||||||
from requests import Session
 | 
					from requests import Session
 | 
				
			||||||
from sentry_sdk.hub import Hub
 | 
					from sentry_sdk.hub import Hub
 | 
				
			||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ 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 structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
from websockets.exceptions import WebSocketException
 | 
					from websockets.exceptions import WebSocketException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ from django.http.request import HttpRequest
 | 
				
			|||||||
from django.template import Context
 | 
					from django.template import Context
 | 
				
			||||||
from django.templatetags.static import static
 | 
					from django.templatetags.static import static
 | 
				
			||||||
from django.utils.html import escape, mark_safe
 | 
					from django.utils.html import escape, mark_safe
 | 
				
			||||||
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.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ from django.http import HttpResponse
 | 
				
			|||||||
from django.shortcuts import redirect, reverse
 | 
					from django.shortcuts import redirect, reverse
 | 
				
			||||||
from django.urls import NoReverseMatch
 | 
					from django.urls import NoReverseMatch
 | 
				
			||||||
from django.utils.http import urlencode
 | 
					from django.utils.http import urlencode
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ from django.db import ProgrammingError
 | 
				
			|||||||
from docker.constants import DEFAULT_UNIX_SOCKET
 | 
					from docker.constants import DEFAULT_UNIX_SOCKET
 | 
				
			||||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
					from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
				
			||||||
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
 | 
					from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ from channels.exceptions import DenyConnection
 | 
				
			|||||||
from dacite import from_dict
 | 
					from dacite import from_dict
 | 
				
			||||||
from dacite.data import Data
 | 
					from dacite.data import Data
 | 
				
			||||||
from guardian.shortcuts import get_objects_for_user
 | 
					from guardian.shortcuts import get_objects_for_user
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.channels import AuthJsonConsumer
 | 
					from authentik.core.channels import AuthJsonConsumer
 | 
				
			||||||
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
 | 
					from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
"""Base Controller"""
 | 
					"""Base Controller"""
 | 
				
			||||||
from dataclasses import dataclass
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
from structlog.testing import capture_logs
 | 
					from structlog.testing import capture_logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from kubernetes.client import V1ObjectMeta
 | 
					from kubernetes.client import V1ObjectMeta
 | 
				
			||||||
from kubernetes.client.rest import ApiException
 | 
					from kubernetes.client.rest import ApiException
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
@ -93,7 +93,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
    def reconcile(self, current: T, reference: T):
 | 
					    def reconcile(self, current: T, reference: T):
 | 
				
			||||||
        """Check what operations should be done, should be raised as
 | 
					        """Check what operations should be done, should be raised as
 | 
				
			||||||
        ReconcileTrigger"""
 | 
					        ReconcileTrigger"""
 | 
				
			||||||
        if current.metadata.annotations != reference.metadata.annotations:
 | 
					        if current.metadata.labels != reference.metadata.labels:
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, reference: T):
 | 
					    def create(self, reference: T):
 | 
				
			||||||
 | 
				
			|||||||
@ -140,5 +140,8 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def update(self, current: V1Deployment, reference: V1Deployment):
 | 
					    def update(self, current: V1Deployment, reference: V1Deployment):
 | 
				
			||||||
        return self.api.patch_namespaced_deployment(
 | 
					        return self.api.patch_namespaced_deployment(
 | 
				
			||||||
            current.metadata.name, self.namespace, reference
 | 
					            current.metadata.name,
 | 
				
			||||||
 | 
					            self.namespace,
 | 
				
			||||||
 | 
					            reference,
 | 
				
			||||||
 | 
					            field_manager=FIELD_MANAGER,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -67,5 +67,8 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def update(self, current: V1Secret, reference: V1Secret):
 | 
					    def update(self, current: V1Secret, reference: V1Secret):
 | 
				
			||||||
        return self.api.patch_namespaced_secret(
 | 
					        return self.api.patch_namespaced_secret(
 | 
				
			||||||
            current.metadata.name, self.namespace, reference
 | 
					            current.metadata.name,
 | 
				
			||||||
 | 
					            self.namespace,
 | 
				
			||||||
 | 
					            reference,
 | 
				
			||||||
 | 
					            field_manager=FIELD_MANAGER,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -67,5 +67,8 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def update(self, current: V1Service, reference: V1Service):
 | 
					    def update(self, current: V1Service, reference: V1Service):
 | 
				
			||||||
        return self.api.patch_namespaced_service(
 | 
					        return self.api.patch_namespaced_service(
 | 
				
			||||||
            current.metadata.name, self.namespace, reference
 | 
					            current.metadata.name,
 | 
				
			||||||
 | 
					            self.namespace,
 | 
				
			||||||
 | 
					            reference,
 | 
				
			||||||
 | 
					            field_manager=FIELD_MANAGER,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@ from kubernetes.config.incluster_config import load_incluster_config
 | 
				
			|||||||
from kubernetes.config.kube_config import load_kube_config_from_dict
 | 
					from kubernetes.config.kube_config import load_kube_config_from_dict
 | 
				
			||||||
from model_utils.managers import InheritanceManager
 | 
					from model_utils.managers import InheritanceManager
 | 
				
			||||||
from packaging.version import LegacyVersion, Version, parse
 | 
					from packaging.version import LegacyVersion, Version, parse
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
from urllib3.exceptions import HTTPError
 | 
					from urllib3.exceptions import HTTPError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
 | 
				
			|||||||
@ -2,17 +2,24 @@
 | 
				
			|||||||
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
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import Provider
 | 
				
			||||||
 | 
					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.models import Outpost
 | 
					from authentik.outposts.models import Outpost, OutpostServiceConnection
 | 
				
			||||||
from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
 | 
					from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					UPDATE_TRIGGERING_MODELS = (
 | 
				
			||||||
 | 
					    Outpost,
 | 
				
			||||||
 | 
					    OutpostServiceConnection,
 | 
				
			||||||
 | 
					    Provider,
 | 
				
			||||||
 | 
					    CertificateKeyPair,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save)
 | 
					@receiver(post_save)
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					 | 
				
			||||||
def post_save_update(sender, instance: Model, **_):
 | 
					def post_save_update(sender, instance: Model, **_):
 | 
				
			||||||
    """If an Outpost is saved, Ensure that token is created/updated
 | 
					    """If an Outpost is saved, Ensure that token is created/updated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,6 +29,8 @@ def post_save_update(sender, instance: Model, **_):
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
    if instance.__module__ == "__fake__":
 | 
					    if instance.__module__ == "__fake__":
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					    if sender not in UPDATE_TRIGGERING_MODELS:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
    outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
 | 
					    outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,9 @@ 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 structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.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 ControllerException
 | 
				
			||||||
from authentik.outposts.models import (
 | 
					from authentik.outposts.models import (
 | 
				
			||||||
@ -49,9 +49,15 @@ def outpost_service_connection_state(connection_pk: Any):
 | 
				
			|||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
def outpost_service_connection_monitor(self: MonitoredTask):
 | 
					def outpost_service_connection_monitor(self: MonitoredTask):
 | 
				
			||||||
    """Regularly check the state of Outpost Service Connections"""
 | 
					    """Regularly check the state of Outpost Service Connections"""
 | 
				
			||||||
    for connection in OutpostServiceConnection.objects.all():
 | 
					    connections = OutpostServiceConnection.objects.all()
 | 
				
			||||||
 | 
					    for connection in connections.iterator():
 | 
				
			||||||
        outpost_service_connection_state.delay(connection.pk)
 | 
					        outpost_service_connection_state.delay(connection.pk)
 | 
				
			||||||
    self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
 | 
					    self.set_status(
 | 
				
			||||||
 | 
					        TaskResult(
 | 
				
			||||||
 | 
					            TaskResultStatus.SUCCESSFUL,
 | 
				
			||||||
 | 
					            [f"Successfully updated {len(connections)} connections."],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
@ -124,14 +130,12 @@ def outpost_post_save(model_class: str, model_pk: Any):
 | 
				
			|||||||
        _ = instance.token
 | 
					        _ = instance.token
 | 
				
			||||||
        LOGGER.debug("Trigger reconcile for outpost")
 | 
					        LOGGER.debug("Trigger reconcile for outpost")
 | 
				
			||||||
        outpost_controller.delay(instance.pk)
 | 
					        outpost_controller.delay(instance.pk)
 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if isinstance(instance, (OutpostModel, Outpost)):
 | 
					    if isinstance(instance, (OutpostModel, Outpost)):
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "triggering outpost update from outpostmodel/outpost", instance=instance
 | 
					            "triggering outpost update from outpostmodel/outpost", instance=instance
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        outpost_send_update(instance)
 | 
					        outpost_send_update(instance)
 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if isinstance(instance, OutpostServiceConnection):
 | 
					    if isinstance(instance, OutpostServiceConnection):
 | 
				
			||||||
        LOGGER.debug("triggering ServiceConnection state update", instance=instance)
 | 
					        LOGGER.debug("triggering ServiceConnection state update", instance=instance)
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.db import models
 | 
				
			|||||||
from django.forms import ModelForm
 | 
					from django.forms import ModelForm
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from rest_framework.serializers import BaseSerializer
 | 
					from rest_framework.serializers import BaseSerializer
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.policies.models import Policy
 | 
					from authentik.policies.models import Policy
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
"""authentik policy engine"""
 | 
					"""authentik policy engine"""
 | 
				
			||||||
from multiprocessing import Pipe, set_start_method
 | 
					from enum import Enum
 | 
				
			||||||
 | 
					from multiprocessing import Pipe, current_process
 | 
				
			||||||
from multiprocessing.connection import Connection
 | 
					from multiprocessing.connection import Connection
 | 
				
			||||||
from typing import Iterator, List, Optional
 | 
					from typing import Iterator, List, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -7,7 +8,7 @@ from django.core.cache import cache
 | 
				
			|||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from sentry_sdk.hub import Hub
 | 
					from sentry_sdk.hub import Hub
 | 
				
			||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
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.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
					from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
				
			||||||
@ -15,9 +16,7 @@ from authentik.policies.process import PolicyProcess, cache_key
 | 
				
			|||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
# This is only really needed for macOS, because Python 3.8 changed the default to spawn
 | 
					CURRENT_PROCESS = current_process()
 | 
				
			||||||
# spawn causes issues with objects that aren't picklable, and also the django setup
 | 
					 | 
				
			||||||
set_start_method("fork")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyProcessInfo:
 | 
					class PolicyProcessInfo:
 | 
				
			||||||
@ -37,12 +36,23 @@ class PolicyProcessInfo:
 | 
				
			|||||||
        self.result = None
 | 
					        self.result = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PolicyEngineMode(Enum):
 | 
				
			||||||
 | 
					    """Decide how results of multiple policies should be combined."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MODE_AND = "and"
 | 
				
			||||||
 | 
					    MODE_OR = "or"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyEngine:
 | 
					class PolicyEngine:
 | 
				
			||||||
    """Orchestrate policy checking, launch tasks and return result"""
 | 
					    """Orchestrate policy checking, launch tasks and return result"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use_cache: bool
 | 
					    use_cache: bool
 | 
				
			||||||
    request: PolicyRequest
 | 
					    request: PolicyRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mode: PolicyEngineMode
 | 
				
			||||||
 | 
					    # Allow objects with no policies attached to pass
 | 
				
			||||||
 | 
					    empty_result: bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    __pbm: PolicyBindingModel
 | 
					    __pbm: PolicyBindingModel
 | 
				
			||||||
    __cached_policies: List[PolicyResult]
 | 
					    __cached_policies: List[PolicyResult]
 | 
				
			||||||
    __processes: List[PolicyProcessInfo]
 | 
					    __processes: List[PolicyProcessInfo]
 | 
				
			||||||
@ -52,6 +62,10 @@ class PolicyEngine:
 | 
				
			|||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
        self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
 | 
					        self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
 | 
					        self.mode = PolicyEngineMode.MODE_AND
 | 
				
			||||||
 | 
					        # For backwards compatibility, set empty_result to true
 | 
				
			||||||
 | 
					        # objects with no policies attached will pass.
 | 
				
			||||||
 | 
					        self.empty_result = True
 | 
				
			||||||
        if not isinstance(pbm, PolicyBindingModel):  # pragma: no cover
 | 
					        if not isinstance(pbm, PolicyBindingModel):  # pragma: no cover
 | 
				
			||||||
            raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
 | 
					            raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
 | 
				
			||||||
        self.__pbm = pbm
 | 
					        self.__pbm = pbm
 | 
				
			||||||
@ -66,8 +80,10 @@ class PolicyEngine:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def _iter_bindings(self) -> Iterator[PolicyBinding]:
 | 
					    def _iter_bindings(self) -> Iterator[PolicyBinding]:
 | 
				
			||||||
        """Make sure all Policies are their respective classes"""
 | 
					        """Make sure all Policies are their respective classes"""
 | 
				
			||||||
        return PolicyBinding.objects.filter(target=self.__pbm, enabled=True).order_by(
 | 
					        return (
 | 
				
			||||||
            "order"
 | 
					            PolicyBinding.objects.filter(target=self.__pbm, enabled=True)
 | 
				
			||||||
 | 
					            .order_by("order")
 | 
				
			||||||
 | 
					            .iterator()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _check_policy_type(self, policy: Policy):
 | 
					    def _check_policy_type(self, policy: Policy):
 | 
				
			||||||
@ -99,14 +115,19 @@ class PolicyEngine:
 | 
				
			|||||||
                LOGGER.debug("P_ENG: Evaluating policy", policy=binding.policy)
 | 
					                LOGGER.debug("P_ENG: Evaluating policy", policy=binding.policy)
 | 
				
			||||||
                our_end, task_end = Pipe(False)
 | 
					                our_end, task_end = Pipe(False)
 | 
				
			||||||
                task = PolicyProcess(binding, self.request, task_end)
 | 
					                task = PolicyProcess(binding, self.request, task_end)
 | 
				
			||||||
 | 
					                task.daemon = False
 | 
				
			||||||
                LOGGER.debug("P_ENG: Starting Process", policy=binding.policy)
 | 
					                LOGGER.debug("P_ENG: Starting Process", policy=binding.policy)
 | 
				
			||||||
                task.start()
 | 
					                if not CURRENT_PROCESS._config.get("daemon"):
 | 
				
			||||||
 | 
					                    task.run()
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    task.start()
 | 
				
			||||||
                self.__processes.append(
 | 
					                self.__processes.append(
 | 
				
			||||||
                    PolicyProcessInfo(process=task, connection=our_end, binding=binding)
 | 
					                    PolicyProcessInfo(process=task, connection=our_end, binding=binding)
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            # If all policies are cached, we have an empty list here.
 | 
					            # If all policies are cached, we have an empty list here.
 | 
				
			||||||
            for proc_info in self.__processes:
 | 
					            for proc_info in self.__processes:
 | 
				
			||||||
                proc_info.process.join(proc_info.binding.timeout)
 | 
					                if proc_info.process.is_alive():
 | 
				
			||||||
 | 
					                    proc_info.process.join(proc_info.binding.timeout)
 | 
				
			||||||
                # Only call .recv() if no result is saved, otherwise we just deadlock here
 | 
					                # Only call .recv() if no result is saved, otherwise we just deadlock here
 | 
				
			||||||
                if not proc_info.result:
 | 
					                if not proc_info.result:
 | 
				
			||||||
                    proc_info.result = proc_info.connection.recv()
 | 
					                    proc_info.result = proc_info.connection.recv()
 | 
				
			||||||
@ -119,24 +140,19 @@ class PolicyEngine:
 | 
				
			|||||||
            x.result for x in self.__processes if x.result
 | 
					            x.result for x in self.__processes if x.result
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        all_results = list(process_results + self.__cached_policies)
 | 
					        all_results = list(process_results + self.__cached_policies)
 | 
				
			||||||
        final_result = PolicyResult(False)
 | 
					 | 
				
			||||||
        final_result.messages = []
 | 
					 | 
				
			||||||
        final_result.source_results = all_results
 | 
					 | 
				
			||||||
        if len(all_results) < self.__expected_result_count:  # pragma: no cover
 | 
					        if len(all_results) < self.__expected_result_count:  # pragma: no cover
 | 
				
			||||||
            raise AssertionError("Got less results than polices")
 | 
					            raise AssertionError("Got less results than polices")
 | 
				
			||||||
        for result in all_results:
 | 
					        # No results, no policies attached -> passing
 | 
				
			||||||
            LOGGER.debug(
 | 
					        if len(all_results) == 0:
 | 
				
			||||||
                "P_ENG: result", passing=result.passing, messages=result.messages
 | 
					            return PolicyResult(self.empty_result)
 | 
				
			||||||
            )
 | 
					        passing = False
 | 
				
			||||||
            if result.messages:
 | 
					        if self.mode == PolicyEngineMode.MODE_AND:
 | 
				
			||||||
                final_result.messages.extend(result.messages)
 | 
					            passing = all([x.passing for x in all_results])
 | 
				
			||||||
            if not result.passing:
 | 
					        if self.mode == PolicyEngineMode.MODE_OR:
 | 
				
			||||||
                final_result.messages = tuple(final_result.messages)
 | 
					            passing = any([x.passing for x in all_results])
 | 
				
			||||||
                final_result.passing = False
 | 
					        result = PolicyResult(passing)
 | 
				
			||||||
                return final_result
 | 
					        result.messages = tuple([y for x in all_results for y in x.messages])
 | 
				
			||||||
        final_result.messages = tuple(final_result.messages)
 | 
					        return result
 | 
				
			||||||
        final_result.passing = True
 | 
					 | 
				
			||||||
        return final_result
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def passing(self) -> bool:
 | 
					    def passing(self) -> bool:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								authentik/policies/event_matcher/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/policies/event_matcher/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										25
									
								
								authentik/policies/event_matcher/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								authentik/policies/event_matcher/api.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					"""Event Matcher Policy API"""
 | 
				
			||||||
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.policies.event_matcher.models import EventMatcherPolicy
 | 
				
			||||||
 | 
					from authentik.policies.forms import GENERAL_SERIALIZER_FIELDS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventMatcherPolicySerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    """Event Matcher Policy Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = EventMatcherPolicy
 | 
				
			||||||
 | 
					        fields = GENERAL_SERIALIZER_FIELDS + [
 | 
				
			||||||
 | 
					            "action",
 | 
				
			||||||
 | 
					            "client_ip",
 | 
				
			||||||
 | 
					            "app",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventMatcherPolicyViewSet(ModelViewSet):
 | 
				
			||||||
 | 
					    """Event Matcher Policy Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = EventMatcherPolicy.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = EventMatcherPolicySerializer
 | 
				
			||||||
							
								
								
									
										11
									
								
								authentik/policies/event_matcher/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								authentik/policies/event_matcher/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					"""authentik Event Matcher policy app config"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.apps import AppConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AuthentikPoliciesEventMatcherConfig(AppConfig):
 | 
				
			||||||
 | 
					    """authentik Event Matcher policy app config"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name = "authentik.policies.event_matcher"
 | 
				
			||||||
 | 
					    label = "authentik_policies_event_matcher"
 | 
				
			||||||
 | 
					    verbose_name = "authentik Policies.Event Matcher"
 | 
				
			||||||
							
								
								
									
										25
									
								
								authentik/policies/event_matcher/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								authentik/policies/event_matcher/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					"""authentik Event Matcher Policy forms"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django import forms
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.policies.event_matcher.models import EventMatcherPolicy
 | 
				
			||||||
 | 
					from authentik.policies.forms import GENERAL_FIELDS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventMatcherPolicyForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    """EventMatcherPolicy Form"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = EventMatcherPolicy
 | 
				
			||||||
 | 
					        fields = GENERAL_FIELDS + [
 | 
				
			||||||
 | 
					            "action",
 | 
				
			||||||
 | 
					            "client_ip",
 | 
				
			||||||
 | 
					            "app",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
 | 
					            "client_ip": forms.TextInput(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        labels = {"client_ip": _("Client IP")}
 | 
				
			||||||
							
								
								
									
										70
									
								
								authentik/policies/event_matcher/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								authentik/policies/event_matcher/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2020-12-24 10:32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies", "0004_policy_execution_logging"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="EventMatcherPolicy",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "policy_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_policies.policy",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "action",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("login", "Login"),
 | 
				
			||||||
 | 
					                            ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                            ("logout", "Logout"),
 | 
				
			||||||
 | 
					                            ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                            ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                            ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                            ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                            ("invitation_created", "Invite Created"),
 | 
				
			||||||
 | 
					                            ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                            ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                            ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                            ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                            ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                            ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                            ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "property_mapping_exception",
 | 
				
			||||||
 | 
					                                "Property Mapping Exception",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                            ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                            ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                            ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                            ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("client_ip", models.TextField(blank=True)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Group Membership Policy",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Group Membership Policies",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_policies.policy",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2020-12-30 20:46
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0001_initial"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="eventmatcherpolicy",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-10 19:07
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0002_auto_20201230_2046"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            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.group_membership",
 | 
				
			||||||
 | 
					                        "authentik Policies.Group Membership",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    ("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.saml", "authentik Sources.SAML"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.captcha", "authentik Stages.Captcha"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.consent", "authentik Stages.Consent"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.dummy", "authentik Stages.Dummy"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.email", "authentik Stages.Email"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.prompt", "authentik Stages.Prompt"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "authentik.stages.identification",
 | 
				
			||||||
 | 
					                        "authentik Stages.Identification",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    ("authentik.stages.invitation", "authentik Stages.User Invitation"),
 | 
				
			||||||
 | 
					                    ("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.stages.otp_static", "authentik OTP.Static"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.otp_time", "authentik OTP.Time"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.otp_validate", "authentik OTP.Validate"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.password", "authentik Stages.Password"),
 | 
				
			||||||
 | 
					                    ("authentik.core", "authentik Core"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="",
 | 
				
			||||||
 | 
					                help_text="Match events created by selected application. When left empty, all applications are matched.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="eventmatcherpolicy",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                help_text="Match created events with this action type. When left empty, all action types will be matched.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="eventmatcherpolicy",
 | 
				
			||||||
 | 
					            name="client_ip",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                help_text="Matches Event's Client IP (strict matching, for network matching use an Expression Policy)",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-12 21:58
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="eventmatcherpolicy",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Event Matcher Policy",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Event Matcher Policies",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        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.group_membership",
 | 
				
			||||||
 | 
					                        "authentik Policies.Group Membership",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    ("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.saml", "authentik Sources.SAML"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.captcha", "authentik Stages.Captcha"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.consent", "authentik Stages.Consent"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.dummy", "authentik Stages.Dummy"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.email", "authentik Stages.Email"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.prompt", "authentik Stages.Prompt"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "authentik.stages.identification",
 | 
				
			||||||
 | 
					                        "authentik Stages.Identification",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    ("authentik.stages.invitation", "authentik Stages.User Invitation"),
 | 
				
			||||||
 | 
					                    ("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.stages.otp_static", "authentik Stages.OTP.Static"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.password", "authentik Stages.Password"),
 | 
				
			||||||
 | 
					                    ("authentik.core", "authentik Core"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="",
 | 
				
			||||||
 | 
					                help_text="Match events created by selected application. When left empty, all applications are matched.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										88
									
								
								authentik/policies/event_matcher/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								authentik/policies/event_matcher/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					"""Event Matcher models"""
 | 
				
			||||||
 | 
					from typing import Type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.apps import apps
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.forms import ModelForm
 | 
				
			||||||
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
 | 
					from rest_framework.serializers import BaseSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					from authentik.policies.models import Policy
 | 
				
			||||||
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def app_choices() -> list[tuple[str, str]]:
 | 
				
			||||||
 | 
					    """Get a list of all installed applications that create events.
 | 
				
			||||||
 | 
					    Returns a list of tuples containing (dotted.app.path, name)"""
 | 
				
			||||||
 | 
					    choices = []
 | 
				
			||||||
 | 
					    for app in apps.get_app_configs():
 | 
				
			||||||
 | 
					        if app.label.startswith("authentik"):
 | 
				
			||||||
 | 
					            choices.append((app.name, app.verbose_name))
 | 
				
			||||||
 | 
					    return choices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventMatcherPolicy(Policy):
 | 
				
			||||||
 | 
					    """Passes when Event matches selected criteria."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    action = models.TextField(
 | 
				
			||||||
 | 
					        choices=EventAction.choices,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        help_text=_(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "Match created events with this action type. "
 | 
				
			||||||
 | 
					                "When left empty, all action types will be matched."
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    app = models.TextField(
 | 
				
			||||||
 | 
					        choices=app_choices(),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default="",
 | 
				
			||||||
 | 
					        help_text=_(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "Match events created by selected application. "
 | 
				
			||||||
 | 
					                "When left empty, all applications are matched."
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    client_ip = models.TextField(
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        help_text=_(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "Matches Event's Client IP (strict matching, "
 | 
				
			||||||
 | 
					                "for network matching use an Expression Policy)"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def serializer(self) -> BaseSerializer:
 | 
				
			||||||
 | 
					        from authentik.policies.event_matcher.api import (
 | 
				
			||||||
 | 
					            EventMatcherPolicySerializer,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return EventMatcherPolicySerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> Type[ModelForm]:
 | 
				
			||||||
 | 
					        from authentik.policies.event_matcher.forms import EventMatcherPolicyForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return EventMatcherPolicyForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
				
			||||||
 | 
					        if "event" not in request.context:
 | 
				
			||||||
 | 
					            return PolicyResult(False)
 | 
				
			||||||
 | 
					        event: Event = request.context["event"]
 | 
				
			||||||
 | 
					        if event.action == self.action:
 | 
				
			||||||
 | 
					            return PolicyResult(True, "Action matched.")
 | 
				
			||||||
 | 
					        if event.client_ip == self.client_ip:
 | 
				
			||||||
 | 
					            return PolicyResult(True, "Client IP matched.")
 | 
				
			||||||
 | 
					        if event.app == self.app:
 | 
				
			||||||
 | 
					            return PolicyResult(True, "App matched.")
 | 
				
			||||||
 | 
					        return PolicyResult(False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        verbose_name = _("Event Matcher Policy")
 | 
				
			||||||
 | 
					        verbose_name_plural = _("Event Matcher Policies")
 | 
				
			||||||
							
								
								
									
										68
									
								
								authentik/policies/event_matcher/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								authentik/policies/event_matcher/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					"""event_matcher tests"""
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
 | 
					from authentik.policies.event_matcher.models import EventMatcherPolicy
 | 
				
			||||||
 | 
					from authentik.policies.types import PolicyRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestEventMatcherPolicy(TestCase):
 | 
				
			||||||
 | 
					    """EventMatcherPolicy tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_match_action(self):
 | 
				
			||||||
 | 
					        """Test match action"""
 | 
				
			||||||
 | 
					        event = Event.new(EventAction.LOGIN)
 | 
				
			||||||
 | 
					        request = PolicyRequest(get_anonymous_user())
 | 
				
			||||||
 | 
					        request.context["event"] = event
 | 
				
			||||||
 | 
					        policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            action=EventAction.LOGIN
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = policy.passes(request)
 | 
				
			||||||
 | 
					        self.assertTrue(response.passing)
 | 
				
			||||||
 | 
					        self.assertTupleEqual(response.messages, ("Action matched.",))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_match_client_ip(self):
 | 
				
			||||||
 | 
					        """Test match client_ip"""
 | 
				
			||||||
 | 
					        event = Event.new(EventAction.LOGIN)
 | 
				
			||||||
 | 
					        event.client_ip = "1.2.3.4"
 | 
				
			||||||
 | 
					        request = PolicyRequest(get_anonymous_user())
 | 
				
			||||||
 | 
					        request.context["event"] = event
 | 
				
			||||||
 | 
					        policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            client_ip="1.2.3.4"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = policy.passes(request)
 | 
				
			||||||
 | 
					        self.assertTrue(response.passing)
 | 
				
			||||||
 | 
					        self.assertTupleEqual(response.messages, ("Client IP matched.",))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_match_app(self):
 | 
				
			||||||
 | 
					        """Test match app"""
 | 
				
			||||||
 | 
					        event = Event.new(EventAction.LOGIN)
 | 
				
			||||||
 | 
					        event.app = "foo"
 | 
				
			||||||
 | 
					        request = PolicyRequest(get_anonymous_user())
 | 
				
			||||||
 | 
					        request.context["event"] = event
 | 
				
			||||||
 | 
					        policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(app="foo")
 | 
				
			||||||
 | 
					        response = policy.passes(request)
 | 
				
			||||||
 | 
					        self.assertTrue(response.passing)
 | 
				
			||||||
 | 
					        self.assertTupleEqual(response.messages, ("App matched.",))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_drop(self):
 | 
				
			||||||
 | 
					        """Test drop event"""
 | 
				
			||||||
 | 
					        event = Event.new(EventAction.LOGIN)
 | 
				
			||||||
 | 
					        event.client_ip = "1.2.3.4"
 | 
				
			||||||
 | 
					        request = PolicyRequest(get_anonymous_user())
 | 
				
			||||||
 | 
					        request.context["event"] = event
 | 
				
			||||||
 | 
					        policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            client_ip="1.2.3.5"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = policy.passes(request)
 | 
				
			||||||
 | 
					        self.assertFalse(response.passing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_invalid(self):
 | 
				
			||||||
 | 
					        """Test passing event"""
 | 
				
			||||||
 | 
					        request = PolicyRequest(get_anonymous_user())
 | 
				
			||||||
 | 
					        policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
 | 
				
			||||||
 | 
					            client_ip="1.2.3.4"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = policy.passes(request)
 | 
				
			||||||
 | 
					        self.assertFalse(response.passing)
 | 
				
			||||||
@ -1,6 +1,14 @@
 | 
				
			|||||||
"""policy exceptions"""
 | 
					"""policy exceptions"""
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyException(SentryIgnoredException):
 | 
					class PolicyException(SentryIgnoredException):
 | 
				
			||||||
    """Exception that should be raised during Policy Evaluation, and can be recovered from."""
 | 
					    """Exception that should be raised during Policy Evaluation, and can be recovered from."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    src_exc: Optional[Exception] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, src_exc: Optional[Exception] = None) -> None:
 | 
				
			||||||
 | 
					        super().__init__()
 | 
				
			||||||
 | 
					        self.src_exc = src_exc
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.forms import ModelForm
 | 
				
			|||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from rest_framework.serializers import BaseSerializer
 | 
					from rest_framework.serializers import BaseSerializer
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.policies.models import Policy
 | 
					from authentik.policies.models import Policy
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,14 @@
 | 
				
			|||||||
"""authentik expression policy evaluator"""
 | 
					"""authentik expression policy evaluator"""
 | 
				
			||||||
from ipaddress import ip_address, ip_network
 | 
					from ipaddress import ip_address, ip_network
 | 
				
			||||||
from traceback import format_tb
 | 
					 | 
				
			||||||
from typing import TYPE_CHECKING, List, Optional
 | 
					from typing import TYPE_CHECKING, List, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.events.utils import model_to_dict, sanitize_dict
 | 
					 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
 | 
					from authentik.flows.planner import PLAN_CONTEXT_SSO
 | 
				
			||||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
					from authentik.lib.expression.evaluator import BaseEvaluator
 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
 | 
					from authentik.policies.exceptions import PolicyException
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
@ -57,32 +55,26 @@ class PolicyEvaluator(BaseEvaluator):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def handle_error(self, exc: Exception, expression_source: str):
 | 
					    def handle_error(self, exc: Exception, expression_source: str):
 | 
				
			||||||
        """Exception Handler"""
 | 
					        """Exception Handler"""
 | 
				
			||||||
        error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)])
 | 
					        # So, this is a bit questionable. Essentially, we are edit the stacktrace
 | 
				
			||||||
        event = Event.new(
 | 
					        # so the user only sees information relevant to them
 | 
				
			||||||
            EventAction.POLICY_EXCEPTION,
 | 
					        # and none of our surrounding error handling
 | 
				
			||||||
            expression=expression_source,
 | 
					        exc.__traceback__ = exc.__traceback__.tb_next
 | 
				
			||||||
            error=error_string,
 | 
					        raise PolicyException(exc)
 | 
				
			||||||
            request=self._context["request"],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if self.policy:
 | 
					 | 
				
			||||||
            event.context["model"] = sanitize_dict(model_to_dict(self.policy))
 | 
					 | 
				
			||||||
        if "http_request" in self._context:
 | 
					 | 
				
			||||||
            event.from_http(self._context["http_request"])
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            event.set_user(self._context["request"].user)
 | 
					 | 
				
			||||||
            event.save()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def evaluate(self, expression_source: str) -> PolicyResult:
 | 
					    def evaluate(self, expression_source: str) -> PolicyResult:
 | 
				
			||||||
        """Parse and evaluate expression. Policy is expected to return a truthy object.
 | 
					        """Parse and evaluate expression. Policy is expected to return a truthy object.
 | 
				
			||||||
        Messages can be added using 'do ak_message()'."""
 | 
					        Messages can be added using 'do ak_message()'."""
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            result = super().evaluate(expression_source)
 | 
					            result = super().evaluate(expression_source)
 | 
				
			||||||
 | 
					        except PolicyException as exc:
 | 
				
			||||||
 | 
					            # PolicyExceptions should be propagated back to the process,
 | 
				
			||||||
 | 
					            # which handles recording and returning a correct result
 | 
				
			||||||
 | 
					            raise exc
 | 
				
			||||||
        except Exception as exc:  # pylint: disable=broad-except
 | 
					        except Exception as exc:  # pylint: disable=broad-except
 | 
				
			||||||
            LOGGER.warning("Expression error", exc=exc)
 | 
					            LOGGER.warning("Expression error", exc=exc)
 | 
				
			||||||
            return PolicyResult(False, str(exc))
 | 
					            return PolicyResult(False, str(exc))
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            policy_result = PolicyResult(False)
 | 
					            policy_result = PolicyResult(False, *self._messages)
 | 
				
			||||||
            policy_result.messages = tuple(self._messages)
 | 
					 | 
				
			||||||
            if result is None:
 | 
					            if result is None:
 | 
				
			||||||
                LOGGER.warning(
 | 
					                LOGGER.warning(
 | 
				
			||||||
                    "Expression policy returned None",
 | 
					                    "Expression policy returned None",
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
 | 
				
			|||||||
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 authentik.events.models import Event, EventAction
 | 
					from authentik.policies.exceptions import PolicyException
 | 
				
			||||||
from authentik.policies.expression.evaluator import PolicyEvaluator
 | 
					from authentik.policies.expression.evaluator import PolicyEvaluator
 | 
				
			||||||
from authentik.policies.expression.models import ExpressionPolicy
 | 
					from authentik.policies.expression.models import ExpressionPolicy
 | 
				
			||||||
from authentik.policies.types import PolicyRequest
 | 
					from authentik.policies.types import PolicyRequest
 | 
				
			||||||
@ -44,30 +44,8 @@ class TestEvaluator(TestCase):
 | 
				
			|||||||
        template = ";"
 | 
					        template = ";"
 | 
				
			||||||
        evaluator = PolicyEvaluator("test")
 | 
					        evaluator = PolicyEvaluator("test")
 | 
				
			||||||
        evaluator.set_policy_request(self.request)
 | 
					        evaluator.set_policy_request(self.request)
 | 
				
			||||||
        result = evaluator.evaluate(template)
 | 
					        with self.assertRaises(PolicyException):
 | 
				
			||||||
        self.assertEqual(result.passing, False)
 | 
					            evaluator.evaluate(template)
 | 
				
			||||||
        self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
 | 
					 | 
				
			||||||
        self.assertTrue(
 | 
					 | 
				
			||||||
            Event.objects.filter(
 | 
					 | 
				
			||||||
                action=EventAction.POLICY_EXCEPTION,
 | 
					 | 
				
			||||||
                context__expression=template,
 | 
					 | 
				
			||||||
            ).exists()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_undefined(self):
 | 
					 | 
				
			||||||
        """test undefined result"""
 | 
					 | 
				
			||||||
        template = "{{ foo.bar }}"
 | 
					 | 
				
			||||||
        evaluator = PolicyEvaluator("test")
 | 
					 | 
				
			||||||
        evaluator.set_policy_request(self.request)
 | 
					 | 
				
			||||||
        result = evaluator.evaluate(template)
 | 
					 | 
				
			||||||
        self.assertEqual(result.passing, False)
 | 
					 | 
				
			||||||
        self.assertEqual(result.messages, ("name 'foo' is not defined",))
 | 
					 | 
				
			||||||
        self.assertTrue(
 | 
					 | 
				
			||||||
            Event.objects.filter(
 | 
					 | 
				
			||||||
                action=EventAction.POLICY_EXCEPTION,
 | 
					 | 
				
			||||||
                context__expression=template,
 | 
					 | 
				
			||||||
            ).exists()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_validate(self):
 | 
					    def test_validate(self):
 | 
				
			||||||
        """test validate"""
 | 
					        """test validate"""
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.forms import ModelForm
 | 
				
			|||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from requests import get
 | 
					from requests import get
 | 
				
			||||||
from rest_framework.serializers import BaseSerializer
 | 
					from rest_framework.serializers import BaseSerializer
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.policies.models import Policy, PolicyResult
 | 
					from authentik.policies.models import Policy, PolicyResult
 | 
				
			||||||
from authentik.policies.types import PolicyRequest
 | 
					from authentik.policies.types import PolicyRequest
 | 
				
			||||||
 | 
				
			|||||||
@ -64,7 +64,10 @@ class PolicyBinding(SerializerModel):
 | 
				
			|||||||
        return PolicyBindingSerializer
 | 
					        return PolicyBindingSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
        return f"Policy Binding {self.target} #{self.order} {self.policy}"
 | 
					        try:
 | 
				
			||||||
 | 
					            return f"Policy Binding {self.target} #{self.order} {self.policy}"
 | 
				
			||||||
 | 
					        except PolicyBinding.target.RelatedObjectDoesNotExist:  # pylint: disable=no-member
 | 
				
			||||||
 | 
					            return f"Policy Binding - #{self.order} {self.policy}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ from django.db import models
 | 
				
			|||||||
from django.forms import ModelForm
 | 
					from django.forms import ModelForm
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from rest_framework.serializers import BaseSerializer
 | 
					from rest_framework.serializers import BaseSerializer
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.policies.models import Policy
 | 
					from authentik.policies.models import Policy
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,13 @@
 | 
				
			|||||||
"""authentik policy task"""
 | 
					"""authentik policy task"""
 | 
				
			||||||
from multiprocessing import Process
 | 
					from multiprocessing import get_context
 | 
				
			||||||
from multiprocessing.connection import Connection
 | 
					from multiprocessing.connection import Connection
 | 
				
			||||||
 | 
					from traceback import format_tb
 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from sentry_sdk.hub import Hub
 | 
					from sentry_sdk.hub import Hub
 | 
				
			||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.policies.exceptions import PolicyException
 | 
					from authentik.policies.exceptions import PolicyException
 | 
				
			||||||
@ -14,19 +15,24 @@ from authentik.policies.models import PolicyBinding
 | 
				
			|||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					TRACEBACK_HEADER = "Traceback (most recent call last):\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
 | 
					def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
 | 
				
			||||||
    """Generate Cache key for policy"""
 | 
					    """Generate Cache key for policy"""
 | 
				
			||||||
    prefix = f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}"
 | 
					    prefix = f"policy_{binding.policy_binding_uuid.hex}_{binding.policy.pk.hex}"
 | 
				
			||||||
    if request.http_request:
 | 
					    if request.http_request and hasattr(request.http_request, "session"):
 | 
				
			||||||
        prefix += f"_{request.http_request.session.session_key}"
 | 
					        prefix += f"_{request.http_request.session.session_key}"
 | 
				
			||||||
    if request.user:
 | 
					    if request.user:
 | 
				
			||||||
        prefix += f"#{request.user.pk}"
 | 
					        prefix += f"#{request.user.pk}"
 | 
				
			||||||
    return prefix
 | 
					    return prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyProcess(Process):
 | 
					FORK_CTX = get_context("fork")
 | 
				
			||||||
 | 
					PROCESS_CLASS = FORK_CTX.Process
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PolicyProcess(PROCESS_CLASS):
 | 
				
			||||||
    """Evaluate a single policy within a seprate process"""
 | 
					    """Evaluate a single policy within a seprate process"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    connection: Connection
 | 
					    connection: Connection
 | 
				
			||||||
@ -47,6 +53,24 @@ class PolicyProcess(Process):
 | 
				
			|||||||
        if connection:
 | 
					        if connection:
 | 
				
			||||||
            self.connection = connection
 | 
					            self.connection = connection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_event(self, action: str, message: str, **kwargs):
 | 
				
			||||||
 | 
					        """Create event with common values from `self.request` and `self.binding`."""
 | 
				
			||||||
 | 
					        # Keep a reference to http_request even if its None, because cleanse_dict will remove it
 | 
				
			||||||
 | 
					        http_request = self.request.http_request
 | 
				
			||||||
 | 
					        event = Event.new(
 | 
				
			||||||
 | 
					            action=action,
 | 
				
			||||||
 | 
					            message=message,
 | 
				
			||||||
 | 
					            policy_uuid=self.binding.policy.policy_uuid.hex,
 | 
				
			||||||
 | 
					            binding=self.binding,
 | 
				
			||||||
 | 
					            request=self.request,
 | 
				
			||||||
 | 
					            **kwargs,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        event.set_user(self.request.user)
 | 
				
			||||||
 | 
					        if http_request:
 | 
				
			||||||
 | 
					            event.from_http(http_request)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            event.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def execute(self) -> PolicyResult:
 | 
					    def execute(self) -> PolicyResult:
 | 
				
			||||||
        """Run actual policy, returns result"""
 | 
					        """Run actual policy, returns result"""
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
@ -58,16 +82,23 @@ class PolicyProcess(Process):
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            policy_result = self.binding.policy.passes(self.request)
 | 
					            policy_result = self.binding.policy.passes(self.request)
 | 
				
			||||||
            if self.binding.policy.execution_logging:
 | 
					            if self.binding.policy.execution_logging:
 | 
				
			||||||
                event = Event.new(
 | 
					                self.create_event(
 | 
				
			||||||
                    EventAction.POLICY_EXECUTION,
 | 
					                    EventAction.POLICY_EXECUTION,
 | 
				
			||||||
                    request=self.request,
 | 
					                    message="Policy Execution",
 | 
				
			||||||
                    result=policy_result,
 | 
					                    result=policy_result,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                event.set_user(self.request.user)
 | 
					 | 
				
			||||||
                event.save()
 | 
					 | 
				
			||||||
        except PolicyException as exc:
 | 
					        except PolicyException as exc:
 | 
				
			||||||
            LOGGER.debug("P_ENG(proc): error", exc=exc)
 | 
					            # Either use passed original exception or whatever we have
 | 
				
			||||||
            policy_result = PolicyResult(False, str(exc))
 | 
					            src_exc = exc.src_exc if exc.src_exc else exc
 | 
				
			||||||
 | 
					            error_string = (
 | 
				
			||||||
 | 
					                TRACEBACK_HEADER
 | 
				
			||||||
 | 
					                + "".join(format_tb(src_exc.__traceback__))
 | 
				
			||||||
 | 
					                + str(src_exc)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            # Create policy exception event
 | 
				
			||||||
 | 
					            self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
 | 
				
			||||||
 | 
					            LOGGER.debug("P_ENG(proc): error", exc=src_exc)
 | 
				
			||||||
 | 
					            policy_result = PolicyResult(False, str(src_exc))
 | 
				
			||||||
        policy_result.source_policy = self.binding.policy
 | 
					        policy_result.source_policy = self.binding.policy
 | 
				
			||||||
        # Invert result if policy.negate is set
 | 
					        # Invert result if policy.negate is set
 | 
				
			||||||
        if self.binding.negate:
 | 
					        if self.binding.negate:
 | 
				
			||||||
@ -96,5 +127,5 @@ class PolicyProcess(Process):
 | 
				
			|||||||
            try:
 | 
					            try:
 | 
				
			||||||
                self.connection.send(self.execute())
 | 
					                self.connection.send(self.execute())
 | 
				
			||||||
            except Exception as exc:  # pylint: disable=broad-except
 | 
					            except Exception as exc:  # pylint: disable=broad-except
 | 
				
			||||||
                LOGGER.warning(exc)
 | 
					                LOGGER.warning(str(exc))
 | 
				
			||||||
                self.connection.send(PolicyResult(False, str(exc)))
 | 
					                self.connection.send(PolicyResult(False, str(exc)))
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ from django.contrib.auth.signals import user_logged_in, user_login_failed
 | 
				
			|||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
from authentik.policies.reputation.models import (
 | 
					from authentik.policies.reputation.models import (
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
"""Reputation tasks"""
 | 
					"""Reputation tasks"""
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
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.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
				
			||||||
from authentik.policies.reputation.models import IPReputation, UserReputation
 | 
					from authentik.policies.reputation.models import IPReputation, UserReputation
 | 
				
			||||||
from authentik.policies.reputation.signals import (
 | 
					from authentik.policies.reputation.signals import (
 | 
				
			||||||
    CACHE_KEY_IP_PREFIX,
 | 
					    CACHE_KEY_IP_PREFIX,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,9 @@
 | 
				
			|||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.api.applications import user_app_cache_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,3 +25,6 @@ def invalidate_policy_cache(sender, instance, **_):
 | 
				
			|||||||
            total += len(keys)
 | 
					            total += len(keys)
 | 
				
			||||||
            cache.delete_many(keys)
 | 
					            cache.delete_many(keys)
 | 
				
			||||||
        LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
 | 
					        LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
 | 
				
			||||||
 | 
					    # Also delete user application cache
 | 
				
			||||||
 | 
					    keys = user_app_cache_key("*")
 | 
				
			||||||
 | 
					    cache.delete_many(keys)
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ from django.test import TestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.policies.dummy.models import DummyPolicy
 | 
					from authentik.policies.dummy.models import DummyPolicy
 | 
				
			||||||
from authentik.policies.engine import PolicyEngine
 | 
					from authentik.policies.engine import PolicyEngine, PolicyEngineMode
 | 
				
			||||||
from authentik.policies.expression.models import ExpressionPolicy
 | 
					from authentik.policies.expression.models import ExpressionPolicy
 | 
				
			||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
					from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
				
			||||||
from authentik.policies.tests.test_process import clear_policy_cache
 | 
					from authentik.policies.tests.test_process import clear_policy_cache
 | 
				
			||||||
@ -44,15 +44,38 @@ class TestPolicyEngine(TestCase):
 | 
				
			|||||||
        self.assertEqual(result.passing, True)
 | 
					        self.assertEqual(result.passing, True)
 | 
				
			||||||
        self.assertEqual(result.messages, ("dummy",))
 | 
					        self.assertEqual(result.messages, ("dummy",))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_engine(self):
 | 
					    def test_engine_mode_and(self):
 | 
				
			||||||
        """Ensure all policies passes (Mix of false and true -> false)"""
 | 
					        """Ensure all policies passes with AND mode (false and true -> false)"""
 | 
				
			||||||
        pbm = PolicyBindingModel.objects.create()
 | 
					        pbm = PolicyBindingModel.objects.create()
 | 
				
			||||||
        PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
 | 
					        PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
 | 
				
			||||||
        PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
 | 
					        PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
 | 
				
			||||||
        engine = PolicyEngine(pbm, self.user)
 | 
					        engine = PolicyEngine(pbm, self.user)
 | 
				
			||||||
        result = engine.build().result
 | 
					        result = engine.build().result
 | 
				
			||||||
        self.assertEqual(result.passing, False)
 | 
					        self.assertEqual(result.passing, False)
 | 
				
			||||||
        self.assertEqual(result.messages, ("dummy",))
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            result.messages,
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "dummy",
 | 
				
			||||||
 | 
					                "dummy",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_engine_mode_or(self):
 | 
				
			||||||
 | 
					        """Ensure all policies passes with OR mode (false and true -> true)"""
 | 
				
			||||||
 | 
					        pbm = PolicyBindingModel.objects.create()
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
 | 
				
			||||||
 | 
					        PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=1)
 | 
				
			||||||
 | 
					        engine = PolicyEngine(pbm, self.user)
 | 
				
			||||||
 | 
					        engine.mode = PolicyEngineMode.MODE_OR
 | 
				
			||||||
 | 
					        result = engine.build().result
 | 
				
			||||||
 | 
					        self.assertEqual(result.passing, True)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            result.messages,
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "dummy",
 | 
				
			||||||
 | 
					                "dummy",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_engine_negate(self):
 | 
					    def test_engine_negate(self):
 | 
				
			||||||
        """Test negate flag"""
 | 
					        """Test negate flag"""
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
"""policy process tests"""
 | 
					"""policy process tests"""
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import RequestFactory, TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import Application, User
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.policies.dummy.models import DummyPolicy
 | 
					from authentik.policies.dummy.models import DummyPolicy
 | 
				
			||||||
from authentik.policies.expression.models import ExpressionPolicy
 | 
					from authentik.policies.expression.models import ExpressionPolicy
 | 
				
			||||||
@ -22,6 +22,7 @@ class TestPolicyProcess(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        clear_policy_cache()
 | 
					        clear_policy_cache()
 | 
				
			||||||
 | 
					        self.factory = RequestFactory()
 | 
				
			||||||
        self.user = User.objects.create_user(username="policyuser")
 | 
					        self.user = User.objects.create_user(username="policyuser")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid(self):
 | 
					    def test_invalid(self):
 | 
				
			||||||
@ -64,7 +65,9 @@ class TestPolicyProcess(TestCase):
 | 
				
			|||||||
    def test_exception(self):
 | 
					    def test_exception(self):
 | 
				
			||||||
        """Test policy execution"""
 | 
					        """Test policy execution"""
 | 
				
			||||||
        policy = Policy.objects.create()
 | 
					        policy = Policy.objects.create()
 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					        binding = PolicyBinding(
 | 
				
			||||||
 | 
					            policy=policy, target=Application.objects.create(name="test")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					        request = PolicyRequest(self.user)
 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					        response = PolicyProcess(binding, request, None).execute()
 | 
				
			||||||
@ -75,31 +78,51 @@ class TestPolicyProcess(TestCase):
 | 
				
			|||||||
        policy = DummyPolicy.objects.create(
 | 
					        policy = DummyPolicy.objects.create(
 | 
				
			||||||
            result=False, wait_min=0, wait_max=1, execution_logging=True
 | 
					            result=False, wait_min=0, wait_max=1, execution_logging=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					        binding = PolicyBinding(
 | 
				
			||||||
 | 
					            policy=policy, target=Application.objects.create(name="test")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        http_request = self.factory.get("/")
 | 
				
			||||||
 | 
					        http_request.user = self.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					        request = PolicyRequest(self.user)
 | 
				
			||||||
 | 
					        request.http_request = http_request
 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					        response = PolicyProcess(binding, request, None).execute()
 | 
				
			||||||
        self.assertEqual(response.passing, False)
 | 
					        self.assertEqual(response.passing, False)
 | 
				
			||||||
        self.assertEqual(response.messages, ("dummy",))
 | 
					        self.assertEqual(response.messages, ("dummy",))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        events = Event.objects.filter(
 | 
					        events = Event.objects.filter(
 | 
				
			||||||
            action=EventAction.POLICY_EXECUTION,
 | 
					            action=EventAction.POLICY_EXECUTION,
 | 
				
			||||||
 | 
					            context__policy_uuid=policy.policy_uuid.hex,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertTrue(events.exists())
 | 
					        self.assertTrue(events.exists())
 | 
				
			||||||
        self.assertEqual(len(events), 1)
 | 
					        self.assertEqual(len(events), 1)
 | 
				
			||||||
        event = events.first()
 | 
					        event = events.first()
 | 
				
			||||||
 | 
					        self.assertEqual(event.user["username"], self.user.username)
 | 
				
			||||||
        self.assertEqual(event.context["result"]["passing"], False)
 | 
					        self.assertEqual(event.context["result"]["passing"], False)
 | 
				
			||||||
        self.assertEqual(event.context["result"]["messages"], ["dummy"])
 | 
					        self.assertEqual(event.context["result"]["messages"], ["dummy"])
 | 
				
			||||||
 | 
					        self.assertEqual(event.client_ip, "127.0.0.1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_raises(self):
 | 
					    def test_raises(self):
 | 
				
			||||||
        """Test policy that raises error"""
 | 
					        """Test policy that raises error"""
 | 
				
			||||||
        policy_raises = ExpressionPolicy.objects.create(
 | 
					        policy_raises = ExpressionPolicy.objects.create(
 | 
				
			||||||
            name="raises", expression="{{ 0/0 }}"
 | 
					            name="raises", expression="{{ 0/0 }}"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        binding = PolicyBinding(policy=policy_raises)
 | 
					        binding = PolicyBinding(
 | 
				
			||||||
 | 
					            policy=policy_raises, target=Application.objects.create(name="test")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					        request = PolicyRequest(self.user)
 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					        response = PolicyProcess(binding, request, None).execute()
 | 
				
			||||||
        self.assertEqual(response.passing, False)
 | 
					        self.assertEqual(response.passing, False)
 | 
				
			||||||
        self.assertEqual(response.messages, ("division by zero",))
 | 
					        self.assertEqual(response.messages, ("division by zero",))
 | 
				
			||||||
        # self.assert
 | 
					
 | 
				
			||||||
 | 
					        events = Event.objects.filter(
 | 
				
			||||||
 | 
					            action=EventAction.POLICY_EXCEPTION,
 | 
				
			||||||
 | 
					            context__policy_uuid=policy_raises.policy_uuid.hex,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertTrue(events.exists())
 | 
				
			||||||
 | 
					        self.assertEqual(len(events), 1)
 | 
				
			||||||
 | 
					        event = events.first()
 | 
				
			||||||
 | 
					        self.assertEqual(event.user["username"], self.user.username)
 | 
				
			||||||
 | 
					        self.assertIn("division by zero", event.context["message"])
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from __future__ import annotations
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dataclasses import dataclass
 | 
					from dataclasses import dataclass
 | 
				
			||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
 | 
					from typing import TYPE_CHECKING, Any, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db.models import Model
 | 
					from django.db.models import Model
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
@ -19,7 +19,7 @@ class PolicyRequest:
 | 
				
			|||||||
    user: User
 | 
					    user: User
 | 
				
			||||||
    http_request: Optional[HttpRequest]
 | 
					    http_request: Optional[HttpRequest]
 | 
				
			||||||
    obj: Optional[Model]
 | 
					    obj: Optional[Model]
 | 
				
			||||||
    context: Dict[str, str]
 | 
					    context: dict[str, Any]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, user: User):
 | 
					    def __init__(self, user: User):
 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
@ -37,10 +37,10 @@ class PolicyResult:
 | 
				
			|||||||
    """Small data-class to hold policy results"""
 | 
					    """Small data-class to hold policy results"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    passing: bool
 | 
					    passing: bool
 | 
				
			||||||
    messages: Tuple[str, ...]
 | 
					    messages: tuple[str, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    source_policy: Optional[Policy]
 | 
					    source_policy: Optional[Policy]
 | 
				
			||||||
    source_results: Optional[List["PolicyResult"]]
 | 
					    source_results: Optional[list["PolicyResult"]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, passing: bool, *messages: str):
 | 
					    def __init__(self, passing: bool, *messages: str):
 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from django.contrib.auth.views import redirect_to_login
 | 
				
			|||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from django.views.generic.base import View
 | 
					from django.views.generic.base import View
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import Application, Provider, User
 | 
					from authentik.core.models import Application, Provider, User
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
 | 
					from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
"""OAuth errors"""
 | 
					"""OAuth errors"""
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
from urllib.parse import quote
 | 
					from urllib.parse import quote
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
from authentik.providers.oauth2.models import GrantTypes
 | 
					from authentik.providers.oauth2.models import GrantTypes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,6 +23,13 @@ class OAuth2Error(SentryIgnoredException):
 | 
				
			|||||||
    def __repr__(self) -> str:
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
        return self.error
 | 
					        return self.error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_event(self, message: Optional[str] = None) -> Event:
 | 
				
			||||||
 | 
					        """Create configuration_error Event and save it."""
 | 
				
			||||||
 | 
					        return Event.new(
 | 
				
			||||||
 | 
					            EventAction.CONFIGURATION_ERROR,
 | 
				
			||||||
 | 
					            message=message or self.description,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RedirectUriError(OAuth2Error):
 | 
					class RedirectUriError(OAuth2Error):
 | 
				
			||||||
    """The request fails due to a missing, invalid, or mismatching
 | 
					    """The request fails due to a missing, invalid, or mismatching
 | 
				
			||||||
@ -28,10 +37,24 @@ class RedirectUriError(OAuth2Error):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    error = "Redirect URI Error"
 | 
					    error = "Redirect URI Error"
 | 
				
			||||||
    description = (
 | 
					    description = (
 | 
				
			||||||
        "The request fails due to a missing, invalid, or mismatching"
 | 
					        "The request fails due to a missing, invalid, or mismatching "
 | 
				
			||||||
        " redirection URI (redirect_uri)."
 | 
					        "redirection URI (redirect_uri)."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    provided_uri: str
 | 
				
			||||||
 | 
					    allowed_uris: list[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None:
 | 
				
			||||||
 | 
					        super().__init__()
 | 
				
			||||||
 | 
					        self.provided_uri = provided_uri
 | 
				
			||||||
 | 
					        self.allowed_uris = allowed_uris
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_event(self) -> Event:
 | 
				
			||||||
 | 
					        return super().to_event(
 | 
				
			||||||
 | 
					            f"Invalid redirect URI was used. Client used '{self.provided_uri}'. "
 | 
				
			||||||
 | 
					            f"Allowed redirect URIs are {','.join(self.allowed_uris)}"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ClientIdError(OAuth2Error):
 | 
					class ClientIdError(OAuth2Error):
 | 
				
			||||||
    """The client identifier (client_id) is missing or invalid."""
 | 
					    """The client identifier (client_id) is missing or invalid."""
 | 
				
			||||||
@ -39,6 +62,15 @@ class ClientIdError(OAuth2Error):
 | 
				
			|||||||
    error = "Client ID Error"
 | 
					    error = "Client ID Error"
 | 
				
			||||||
    description = "The client identifier (client_id) is missing or invalid."
 | 
					    description = "The client identifier (client_id) is missing or invalid."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client_id: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, client_id: str) -> None:
 | 
				
			||||||
 | 
					        super().__init__()
 | 
				
			||||||
 | 
					        self.client_id = client_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_event(self) -> Event:
 | 
				
			||||||
 | 
					        return super().to_event(f"Invalid client identifier: {self.client_id}.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserAuthError(OAuth2Error):
 | 
					class UserAuthError(OAuth2Error):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user