Compare commits
	
		
			98 Commits
		
	
	
		
			version/0.
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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.1-rc1 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) | ||||||
| @ -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] | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.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.1-rc1 | ||||||
|           -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.1-rc1 | ||||||
|       - 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: | ||||||
| @ -48,11 +48,11 @@ jobs: | |||||||
|           cd proxy/ |           cd proxy/ | ||||||
|           docker build \ |           docker build \ | ||||||
|           --no-cache \ |           --no-cache \ | ||||||
|           -t beryju/authentik-proxy:0.14.2-stable \ |           -t beryju/authentik-proxy:2021.1.1-rc1 \ | ||||||
|           -t beryju/authentik-proxy:latest \ |           -t beryju/authentik-proxy:latest \ | ||||||
|           -f Dockerfile . |           -f 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.1-rc1 | ||||||
|       - 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.1-rc1 \ | ||||||
|           -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.1-rc1 | ||||||
|       - 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.1-rc1 | ||||||
|           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:b5052144034e490358c659d0e480c17a4e604fd3aee9a97ddfe6e361a245a4a5", | ||||||
|                 "sha256:d6991e6fd7d0f63bf94282687700a91f5299b807e544cb3367e9b2faeeaf8c62" |                 "sha256:efd6c96c98900e9fbf217f13cb58f59b793e51f69a1ce61817eefd31f17c6ef5" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.16.46" |             "version": "==1.16.55" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:85ca6915ad5471e7f6cd1b00610b74601d2970cbf8e9b1bf255697154cf621a3", |                 "sha256:760d0c16c1474c2a46e3fa45e33ae7457b5cab7410737ab1692340ade764cc73", | ||||||
|                 "sha256:f7d365c689070368a5a0857aa35a81d7c950556189f23065f42798f810a59cae" |                 "sha256:b34327d84b3bb5620fb54603677a9a973b167290c2c1e7ab69c4a46b201c6d46" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.19.46" |             "version": "==1.19.55" | ||||||
|         }, |         }, | ||||||
|         "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.1-rc1" | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| 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 | ||||||
|  | |||||||
| @ -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,10 @@ 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 +58,7 @@ 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) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -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,15 @@ 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) | ||||||
|         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,7 +8,7 @@ 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.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
|  | |||||||
| @ -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,29 @@ 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.tasks import send_mail | ||||||
|  | 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""" | ||||||
|  |  | ||||||
| @ -104,10 +114,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 +139,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 +149,217 @@ 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: | ||||||
|  |             # pyright: reportGeneralTypeIssues=false | ||||||
|  |             return send_mail(mail.__dict__)  # pylint: disable=no-value-for-parameter | ||||||
|  |         except (SMTPException, ConnectionError) 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") | ||||||
|  | |||||||
| @ -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.lib.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, 0) | ||||||
| @ -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() | ||||||
|  | |||||||
| @ -52,6 +52,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""" | ||||||
|  | |||||||
| @ -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,7 +6,7 @@ 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.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
| from authentik.lib.utils.reflection import path_to_class | from authentik.lib.utils.reflection import path_to_class | ||||||
| @ -124,14 +124,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,4 +1,5 @@ | |||||||
| """authentik policy engine""" | """authentik policy engine""" | ||||||
|  | from enum import Enum | ||||||
| from multiprocessing import Pipe, set_start_method | from multiprocessing import Pipe, set_start_method | ||||||
| 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 | ||||||
| @ -37,12 +38,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 +64,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 +82,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): | ||||||
| @ -119,24 +137,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 Process | ||||||
| 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,12 +15,13 @@ 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}" | ||||||
| @ -47,6 +49,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 +78,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 +123,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,6 +1,6 @@ | |||||||
| """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.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
|  | |||||||
| @ -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): | ||||||
|     """ |     """ | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from typing import List, Optional, Tuple | |||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | from django.http import HttpRequest, HttpResponse, JsonResponse | ||||||
| from django.utils.cache import patch_vary_headers | from django.utils.cache import patch_vary_headers | ||||||
| from structlog import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.providers.oauth2.errors import BearerTokenError | from authentik.providers.oauth2.errors import BearerTokenError | ||||||
| from authentik.providers.oauth2.models import RefreshToken | from authentik.providers.oauth2.models import RefreshToken | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from django.http import HttpRequest, HttpResponse | |||||||
| from django.http.response import Http404 | from django.http.response import Http404 | ||||||
| from django.shortcuts import get_object_or_404, redirect | from django.shortcuts import get_object_or_404, redirect | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from structlog import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| @ -145,7 +145,7 @@ class OAuthAuthorizationParams: | |||||||
|             ) |             ) | ||||||
|         except OAuth2Provider.DoesNotExist: |         except OAuth2Provider.DoesNotExist: | ||||||
|             LOGGER.warning("Invalid client identifier", client_id=self.client_id) |             LOGGER.warning("Invalid client identifier", client_id=self.client_id) | ||||||
|             raise ClientIdError() |             raise ClientIdError(client_id=self.client_id) | ||||||
|         self.check_redirect_uri() |         self.check_redirect_uri() | ||||||
|         self.check_scope() |         self.check_scope() | ||||||
|         self.check_nonce() |         self.check_nonce() | ||||||
| @ -155,24 +155,18 @@ class OAuthAuthorizationParams: | |||||||
|         """Redirect URI validation.""" |         """Redirect URI validation.""" | ||||||
|         if not self.redirect_uri: |         if not self.redirect_uri: | ||||||
|             LOGGER.warning("Missing redirect uri.") |             LOGGER.warning("Missing redirect uri.") | ||||||
|             raise RedirectUriError() |             raise RedirectUriError("", self.provider.redirect_uris.split()) | ||||||
|         if self.redirect_uri.lower() not in [ |         if self.redirect_uri.lower() not in [ | ||||||
|             x.lower() for x in self.provider.redirect_uris.split() |             x.lower() for x in self.provider.redirect_uris.split() | ||||||
|         ]: |         ]: | ||||||
|             Event.new( |  | ||||||
|                 EventAction.CONFIGURATION_ERROR, |  | ||||||
|                 provider=self.provider, |  | ||||||
|                 message="Invalid redirect URI was used.", |  | ||||||
|                 client_used=self.redirect_uri, |  | ||||||
|                 configured=self.provider.redirect_uris.split(), |  | ||||||
|             ).save() |  | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Invalid redirect uri", |                 "Invalid redirect uri", | ||||||
|                 redirect_uri=self.redirect_uri, |                 redirect_uri=self.redirect_uri, | ||||||
|                 excepted=self.provider.redirect_uris.split(), |                 excepted=self.provider.redirect_uris.split(), | ||||||
|             ) |             ) | ||||||
|             raise RedirectUriError() |             raise RedirectUriError( | ||||||
|  |                 self.redirect_uri, self.provider.redirect_uris.split() | ||||||
|  |             ) | ||||||
|         if self.request: |         if self.request: | ||||||
|             raise AuthorizeError( |             raise AuthorizeError( | ||||||
|                 self.redirect_uri, "request_not_supported", self.grant_type, self.state |                 self.redirect_uri, "request_not_supported", self.grant_type, self.state | ||||||
| @ -262,10 +256,12 @@ class OAuthFulfillmentStage(StageView): | |||||||
|             ).from_http(self.request) |             ).from_http(self.request) | ||||||
|             return redirect(self.create_response_uri()) |             return redirect(self.create_response_uri()) | ||||||
|         except (ClientIdError, RedirectUriError) as error: |         except (ClientIdError, RedirectUriError) as error: | ||||||
|  |             error.to_event().from_http(request) | ||||||
|             self.executor.stage_invalid() |             self.executor.stage_invalid() | ||||||
|             # pylint: disable=no-member |             # pylint: disable=no-member | ||||||
|             return bad_request_message(request, error.description, title=error.error) |             return bad_request_message(request, error.description, title=error.error) | ||||||
|         except AuthorizeError as error: |         except AuthorizeError as error: | ||||||
|  |             error.to_event().from_http(request) | ||||||
|             self.executor.stage_invalid() |             self.executor.stage_invalid() | ||||||
|             return redirect(error.create_uri()) |             return redirect(error.create_uri()) | ||||||
|  |  | ||||||
| @ -383,8 +379,10 @@ class AuthorizationFlowInitView(PolicyAccessView): | |||||||
|         try: |         try: | ||||||
|             self.params = OAuthAuthorizationParams.from_request(self.request) |             self.params = OAuthAuthorizationParams.from_request(self.request) | ||||||
|         except AuthorizeError as error: |         except AuthorizeError as error: | ||||||
|  |             error.to_event().from_http(self.request) | ||||||
|             raise RequestValidationError(redirect(error.create_uri())) |             raise RequestValidationError(redirect(error.create_uri())) | ||||||
|         except OAuth2Error as error: |         except OAuth2Error as error: | ||||||
|  |             error.to_event().from_http(self.request) | ||||||
|             raise RequestValidationError( |             raise RequestValidationError( | ||||||
|                 bad_request_message(self.request, error.description, title=error.error) |                 bad_request_message(self.request, error.description, title=error.error) | ||||||
|             ) |             ) | ||||||
| @ -398,6 +396,7 @@ class AuthorizationFlowInitView(PolicyAccessView): | |||||||
|                 self.params.grant_type, |                 self.params.grant_type, | ||||||
|                 self.params.state, |                 self.params.state, | ||||||
|             ) |             ) | ||||||
|  |             error.to_event().from_http(self.request) | ||||||
|             raise RequestValidationError(redirect(error.create_uri())) |             raise RequestValidationError(redirect(error.create_uri())) | ||||||
|  |  | ||||||
|     def resolve_provider_application(self): |     def resolve_provider_application(self): | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	