Compare commits
	
		
			102 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| adc4cd9c0d | |||
| abed254ca1 | |||
| edfab0995f | |||
| 528dedf99d | |||
| 5d7eec3049 | |||
| ad44567ebe | |||
| ac82002339 | |||
| df92111296 | |||
| da8417a141 | |||
| 7f32355e3e | |||
| 5afe88a605 | |||
| 320dab3425 | |||
| ca44f8bd60 | |||
| 5fd408ca82 | |||
| becb9e34b5 | |||
| 4917ab9985 | |||
| bd92505bc2 | |||
| 30033d1f90 | |||
| 3e5dfcbd0f | |||
| bf0141acc6 | |||
| 0c8d513567 | |||
| d07704fdf1 | |||
| 086a8753c0 | |||
| ae7a6e2fd6 | |||
| 6a4ddcaba7 | |||
| 2c9b596f01 | |||
| 7257108091 | |||
| 91f7b289cc | |||
| 77a507d2f8 | |||
| 3e60e956f4 | |||
| 84ec70c2a2 | |||
| 72846f0ae1 | |||
| dd53e7e9b1 | |||
| 9df16a9ae0 | |||
| 02dd44eeec | |||
| 2f78e14381 | |||
| ef6f692526 | |||
| 2dd575874b | |||
| 84c2ebabaa | |||
| 3e26170f4b | |||
| 4709dca33c | |||
| 6064a481fb | |||
| 3979b0bde7 | |||
| 4280847bcc | |||
| ade8644da6 | |||
| 3c3fd53999 | |||
| 7b823f23ae | |||
| a67bea95d4 | |||
| 775e0ef2fa | |||
| d102c59654 | |||
| 03448a9169 | |||
| 1e6c081e5c | |||
| 8b9ce4a745 | |||
| 014d93d485 | |||
| 680b182d95 | |||
| b2a832175e | |||
| b3ce8331f5 | |||
| ef0f618234 | |||
| b8a7186a55 | |||
| b39530f873 | |||
| 7937c84f2b | |||
| 621843c60c | |||
| c19da839b1 | |||
| fea1f3be6f | |||
| 6f5ec7838f | |||
| 94300492e7 | |||
| 5d3931c128 | |||
| 262a8b5ae8 | |||
| fe069c5e55 | |||
| c6e60c0ebc | |||
| 90b457c5ee | |||
| 5e724e4299 | |||
| b4c8dd6b91 | |||
| 63d163cc65 | |||
| 2b1356bb91 | |||
| ba9edd6c44 | |||
| 3b2b3262d7 | |||
| 5431e7fe9d | |||
| 7d9c74ce04 | |||
| 60c3cf890a | |||
| 4ec5df6b12 | |||
| 0403f6d373 | |||
| b7f4d15a94 | |||
| 56450887ca | |||
| 9bd613a31d | |||
| 3fe0483dbf | |||
| 63a28ca1e9 | |||
| 2543b075be | |||
| b8bdf7a035 | |||
| a3ff7cea23 | |||
| bb776c2710 | |||
| c9ad87d419 | |||
| 0d81eaffff | |||
| 6930c84425 | |||
| eaaeaccf5d | |||
| efbbd0adcf | |||
| c8d9771640 | |||
| 2b98637ca5 | |||
| e3f7185564 | |||
| d1198fc6c1 | |||
| 8cb5f8fbee | |||
| fad5b09aee | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2021.6.2 | ||||
| current_version = 2021.6.4 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | ||||
| @ -21,6 +21,8 @@ values = | ||||
|  | ||||
| [bumpversion:file:docker-compose.yml] | ||||
|  | ||||
| [bumpversion:file:schema.yml] | ||||
|  | ||||
| [bumpversion:file:.github/workflows/release.yml] | ||||
|  | ||||
| [bumpversion:file:authentik/__init__.py] | ||||
|  | ||||
							
								
								
									
										23
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @ -33,14 +33,14 @@ jobs: | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'release' }} | ||||
|           tags: | | ||||
|             beryju/authentik:2021.6.2, | ||||
|             beryju/authentik:2021.6.4, | ||||
|             beryju/authentik:latest, | ||||
|             ghcr.io/goauthentik/server:2021.6.2, | ||||
|             ghcr.io/goauthentik/server:2021.6.4, | ||||
|             ghcr.io/goauthentik/server:latest | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           context: . | ||||
|       - name: Building Docker Image (stable) | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }} | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }} | ||||
|         run: | | ||||
|           docker pull beryju/authentik:latest | ||||
|           docker tag beryju/authentik:latest beryju/authentik:stable | ||||
| @ -75,14 +75,14 @@ jobs: | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'release' }} | ||||
|           tags: | | ||||
|             beryju/authentik-proxy:2021.6.2, | ||||
|             beryju/authentik-proxy:2021.6.4, | ||||
|             beryju/authentik-proxy:latest, | ||||
|             ghcr.io/goauthentik/proxy:2021.6.2, | ||||
|             ghcr.io/goauthentik/proxy:2021.6.4, | ||||
|             ghcr.io/goauthentik/proxy:latest | ||||
|           file: outpost/proxy.Dockerfile | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|       - name: Building Docker Image (stable) | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }} | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }} | ||||
|         run: | | ||||
|           docker pull beryju/authentik-proxy:latest | ||||
|           docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable | ||||
| @ -117,14 +117,14 @@ jobs: | ||||
|         with: | ||||
|           push: ${{ github.event_name == 'release' }} | ||||
|           tags: | | ||||
|             beryju/authentik-ldap:2021.6.2, | ||||
|             beryju/authentik-ldap:2021.6.4, | ||||
|             beryju/authentik-ldap:latest, | ||||
|             ghcr.io/goauthentik/ldap:2021.6.2, | ||||
|             ghcr.io/goauthentik/ldap:2021.6.4, | ||||
|             ghcr.io/goauthentik/ldap:latest | ||||
|           file: outpost/ldap.Dockerfile | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|       - name: Building Docker Image (stable) | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }} | ||||
|         if: ${{ github.event_name == 'release' && !contains('2021.6.4', 'rc') }} | ||||
|         run: | | ||||
|           docker pull beryju/authentik-ldap:latest | ||||
|           docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable | ||||
| @ -157,7 +157,7 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Setup Node.js environment | ||||
|         uses: actions/setup-node@v2.1.5 | ||||
|         uses: actions/setup-node@v2.2.0 | ||||
|         with: | ||||
|           node-version: 12.x | ||||
|       - name: Build web api client and web ui | ||||
| @ -176,6 +176,7 @@ jobs: | ||||
|           SENTRY_PROJECT: authentik | ||||
|           SENTRY_URL: https://sentry.beryju.org | ||||
|         with: | ||||
|           version: authentik@2021.6.2 | ||||
|           version: authentik@2021.6.4 | ||||
|           environment: beryjuorg-prod | ||||
|           sourcemaps: './web/dist' | ||||
|           finalize: false | ||||
|  | ||||
							
								
								
									
										184
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										184
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -76,11 +76,11 @@ | ||||
|         }, | ||||
|         "asgiref": { | ||||
|             "hashes": [ | ||||
|                 "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", | ||||
|                 "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" | ||||
|                 "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", | ||||
|                 "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==3.3.4" | ||||
|             "version": "==3.4.1" | ||||
|         }, | ||||
|         "async-timeout": { | ||||
|             "hashes": [ | ||||
| @ -122,19 +122,19 @@ | ||||
|         }, | ||||
|         "boto3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31", | ||||
|                 "sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73" | ||||
|                 "sha256:3b35689c215c982fe9f7ef78d748aa9b0cd15c3b2eb04f9b460aaa63fe2fbd03", | ||||
|                 "sha256:b1cbeb92123799001b97f2ee1cdf470e21f1be08314ae28fc7ea357925186f1c" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.17.98" | ||||
|             "version": "==1.17.105" | ||||
|         }, | ||||
|         "botocore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f", | ||||
|                 "sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6" | ||||
|                 "sha256:b0fda4edf8eb105453890700d49011ada576d0cc7326a0699dfabe9e872f552c", | ||||
|                 "sha256:b5ba72d22212b0355f339c2a98b3296b3b2202a48e6a2b1366e866bc65a64b67" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", | ||||
|             "version": "==1.20.98" | ||||
|             "version": "==1.20.105" | ||||
|         }, | ||||
|         "cachetools": { | ||||
|             "hashes": [ | ||||
| @ -165,11 +165,11 @@ | ||||
|         }, | ||||
|         "celery": { | ||||
|             "hashes": [ | ||||
|                 "sha256:54436cd97b031bf2e08064223240e2a83d601d9414bcb1b702f94c6c33c29485", | ||||
|                 "sha256:b5399d76cf70d5cfac3ec993f8796ec1aa90d4cef55972295751f384758a80d7" | ||||
|                 "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0", | ||||
|                 "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==5.1.1" | ||||
|             "version": "==5.1.2" | ||||
|         }, | ||||
|         "certifi": { | ||||
|             "hashes": [ | ||||
| @ -242,11 +242,11 @@ | ||||
|         }, | ||||
|         "channels-redis": { | ||||
|             "hashes": [ | ||||
|                 "sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de", | ||||
|                 "sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6" | ||||
|                 "sha256:0a18ce279c15ba79b7985bb12b2d6dd0ac8a14e4ad6952681f4422a4cc4a5ea9", | ||||
|                 "sha256:1abd5820ff1ed4ac627f8a219ad389e4c87e52e47a230929a7a474e95dd2c6c2" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.2.0" | ||||
|             "version": "==3.3.0" | ||||
|         }, | ||||
|         "chardet": { | ||||
|             "hashes": [ | ||||
| @ -342,11 +342,11 @@ | ||||
|         }, | ||||
|         "django": { | ||||
|             "hashes": [ | ||||
|                 "sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296", | ||||
|                 "sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f" | ||||
|                 "sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd", | ||||
|                 "sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.2.4" | ||||
|             "version": "==3.2.5" | ||||
|         }, | ||||
|         "django-dbbackup": { | ||||
|             "git": "https://github.com/django-dbbackup/django-dbbackup.git", | ||||
| @ -473,11 +473,11 @@ | ||||
|         }, | ||||
|         "google-auth": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef", | ||||
|                 "sha256:e34e5f5de5610b202f9b40ebd9f8b27571d5c5537db9afed3a72b2db5a345039" | ||||
|                 "sha256:9266252e11393943410354cf14a77bcca24dd2ccd9c4e1aef23034fe0fbae630", | ||||
|                 "sha256:c7c215c74348ef24faef2f7b62f6d8e6b38824fe08b1e7b7b09a02d397eda7b3" | ||||
|             ], | ||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", | ||||
|             "version": "==1.32.0" | ||||
|             "version": "==1.32.1" | ||||
|         }, | ||||
|         "gunicorn": { | ||||
|             "hashes": [ | ||||
| @ -778,11 +778,11 @@ | ||||
|         }, | ||||
|         "packaging": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", | ||||
|                 "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" | ||||
|                 "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", | ||||
|                 "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==20.9" | ||||
|             "version": "==21.0" | ||||
|         }, | ||||
|         "prometheus-client": { | ||||
|             "hashes": [ | ||||
| @ -948,10 +948,30 @@ | ||||
|         }, | ||||
|         "pyrsistent": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" | ||||
|                 "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", | ||||
|                 "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", | ||||
|                 "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", | ||||
|                 "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", | ||||
|                 "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", | ||||
|                 "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", | ||||
|                 "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", | ||||
|                 "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", | ||||
|                 "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", | ||||
|                 "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", | ||||
|                 "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", | ||||
|                 "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", | ||||
|                 "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", | ||||
|                 "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", | ||||
|                 "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", | ||||
|                 "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", | ||||
|                 "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", | ||||
|                 "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", | ||||
|                 "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", | ||||
|                 "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", | ||||
|                 "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==0.17.3" | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==0.18.0" | ||||
|         }, | ||||
|         "python-dateutil": { | ||||
|             "hashes": [ | ||||
| @ -1167,11 +1187,11 @@ | ||||
|                 "secure" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", | ||||
|                 "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" | ||||
|                 "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", | ||||
|                 "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.26.5" | ||||
|             "version": "==1.26.6" | ||||
|         }, | ||||
|         "uvicorn": { | ||||
|             "extras": [ | ||||
| @ -1403,11 +1423,11 @@ | ||||
|         }, | ||||
|         "astroid": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", | ||||
|                 "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" | ||||
|                 "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892", | ||||
|                 "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9" | ||||
|             ], | ||||
|             "markers": "python_version ~= '3.6'", | ||||
|             "version": "==2.5.6" | ||||
|             "version": "==2.6.2" | ||||
|         }, | ||||
|         "attrs": { | ||||
|             "hashes": [ | ||||
| @ -1612,11 +1632,11 @@ | ||||
|         }, | ||||
|         "packaging": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", | ||||
|                 "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" | ||||
|                 "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", | ||||
|                 "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==20.9" | ||||
|             "version": "==21.0" | ||||
|         }, | ||||
|         "pathspec": { | ||||
|             "hashes": [ | ||||
| @ -1651,11 +1671,11 @@ | ||||
|         }, | ||||
|         "pylint": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8", | ||||
|                 "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484" | ||||
|                 "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a", | ||||
|                 "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==2.8.3" | ||||
|             "version": "==2.9.3" | ||||
|         }, | ||||
|         "pylint-django": { | ||||
|             "hashes": [ | ||||
| @ -1733,49 +1753,45 @@ | ||||
|         }, | ||||
|         "regex": { | ||||
|             "hashes": [ | ||||
|                 "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5", | ||||
|                 "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79", | ||||
|                 "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31", | ||||
|                 "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500", | ||||
|                 "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11", | ||||
|                 "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14", | ||||
|                 "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3", | ||||
|                 "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439", | ||||
|                 "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c", | ||||
|                 "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82", | ||||
|                 "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711", | ||||
|                 "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093", | ||||
|                 "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a", | ||||
|                 "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb", | ||||
|                 "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8", | ||||
|                 "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17", | ||||
|                 "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000", | ||||
|                 "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d", | ||||
|                 "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480", | ||||
|                 "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc", | ||||
|                 "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0", | ||||
|                 "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9", | ||||
|                 "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765", | ||||
|                 "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e", | ||||
|                 "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a", | ||||
|                 "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07", | ||||
|                 "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f", | ||||
|                 "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac", | ||||
|                 "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7", | ||||
|                 "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed", | ||||
|                 "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968", | ||||
|                 "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7", | ||||
|                 "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2", | ||||
|                 "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4", | ||||
|                 "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87", | ||||
|                 "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8", | ||||
|                 "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10", | ||||
|                 "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29", | ||||
|                 "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605", | ||||
|                 "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6", | ||||
|                 "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042" | ||||
|                 "sha256:0e46c1191b2eb293a6912269ed08b4512e7e241bbf591f97e527492e04c77e93", | ||||
|                 "sha256:18040755606b0c21281493ec309214bd61e41a170509e5014f41d6a5a586e161", | ||||
|                 "sha256:1806370b2bef4d4193eebe8ee59a9fd7547836a34917b7badbe6561a8594d9cb", | ||||
|                 "sha256:1ccbd41dbee3a31e18938096510b7d4ee53aa9fce2ee3dcc8ec82ae264f6acfd", | ||||
|                 "sha256:1d386402ae7f3c9b107ae5863f7ecccb0167762c82a687ae6526b040feaa5ac6", | ||||
|                 "sha256:210c359e6ee5b83f7d8c529ba3c75ba405481d50f35a420609b0db827e2e3bb5", | ||||
|                 "sha256:268fe9dd1deb4a30c8593cabd63f7a241dfdc5bd9dd0233906c718db22cdd49a", | ||||
|                 "sha256:361be4d311ac995a8c7ad577025a3ae3a538531b1f2cf32efd8b7e5d33a13e5a", | ||||
|                 "sha256:3f7a92e60930f8fca2623d9e326c173b7cf2c8b7e4fdcf984b75a1d2fb08114d", | ||||
|                 "sha256:444723ebaeb7fa8125f29c01a31101a3854ac3de293e317944022ae5effa53a4", | ||||
|                 "sha256:494d0172774dc0beeea984b94c95389143db029575f7ca908edd74469321ea99", | ||||
|                 "sha256:4b1999ef60c45357598935c12508abf56edbbb9c380df6f336de38a6c3a294ae", | ||||
|                 "sha256:4fc86b729ab88fe8ac3ec92287df253c64aa71560d76da5acd8a2e245839c629", | ||||
|                 "sha256:5049d00dbb78f9d166d1c704e93934d42cce0570842bb1a61695123d6b01de09", | ||||
|                 "sha256:56bef6b414949e2c9acf96cb5d78de8b529c7b99752619494e78dc76f99fd005", | ||||
|                 "sha256:59845101de68fd5d3a1145df9ea022e85ecd1b49300ea68307ad4302320f6f61", | ||||
|                 "sha256:6b8b629f93246e507287ee07e26744beaffb4c56ed520576deac8b615bd76012", | ||||
|                 "sha256:6c72ebb72e64e9bd195cb35a9b9bbfb955fd953b295255b8ae3e4ad4a146b615", | ||||
|                 "sha256:7743798dfb573d006f1143d745bf17efad39775a5190b347da5d83079646be56", | ||||
|                 "sha256:78a2a885345a2d60b5e68099e877757d5ed12e46ba1e87507175f14f80892af3", | ||||
|                 "sha256:849802379a660206277675aa5a5c327f5c910c690649535863ddf329b0ba8c87", | ||||
|                 "sha256:8cf6728f89b071bd3ab37cb8a0e306f4de897553a0ed07442015ee65fbf53d62", | ||||
|                 "sha256:a1b6a3f600d6aff97e3f28c34192c9ed93fee293bd96ef327b64adb51a74b2f6", | ||||
|                 "sha256:a548bb51c4476332ce4139df8e637386730f79a92652a907d12c696b6252b64d", | ||||
|                 "sha256:a8a5826d8a1b64e2ff9af488cc179e1a4d0f144d11ce486a9f34ea38ccedf4ef", | ||||
|                 "sha256:b024ee43ee6b310fad5acaee23e6485b21468718cb792a9d1693eecacc3f0b7e", | ||||
|                 "sha256:b092754c06852e8a8b022004aff56c24b06310189186805800d09313c37ce1f8", | ||||
|                 "sha256:b1dbeef938281f240347d50f28ae53c4b046a23389cd1fc4acec5ea0eae646a1", | ||||
|                 "sha256:bf819c5b77ff44accc9a24e31f1f7ceaaf6c960816913ed3ef8443b9d20d81b6", | ||||
|                 "sha256:c11f2fca544b5e30a0e813023196a63b1cb9869106ef9a26e9dae28bce3e4e26", | ||||
|                 "sha256:ce269e903b00d1ab4746793e9c50a57eec5d5388681abef074d7b9a65748fca5", | ||||
|                 "sha256:d0cf2651a8804f6325747c7e55e3be0f90ee2848e25d6b817aa2728d263f9abb", | ||||
|                 "sha256:e07e92935040c67f49571779d115ecb3e727016d42fb36ee0d8757db4ca12ee0", | ||||
|                 "sha256:e80d2851109e56420b71f9702ad1646e2f0364528adbf6af85527bc61e49f394", | ||||
|                 "sha256:ed77b97896312bc2deafe137ca2626e8b63808f5bedb944f73665c68093688a7", | ||||
|                 "sha256:f32f47fb22c988c0b35756024b61d156e5c4011cb8004aa53d93b03323c45657", | ||||
|                 "sha256:fdad3122b69cdabdb3da4c2a4107875913ac78dab0117fc73f988ad589c66b66" | ||||
|             ], | ||||
|             "version": "==2021.4.4" | ||||
|             "version": "==2021.7.1" | ||||
|         }, | ||||
|         "requests": { | ||||
|             "hashes": [ | ||||
| @ -1838,11 +1854,11 @@ | ||||
|                 "secure" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", | ||||
|                 "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" | ||||
|                 "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", | ||||
|                 "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.26.5" | ||||
|             "version": "==1.26.6" | ||||
|         }, | ||||
|         "wrapt": { | ||||
|             "hashes": [ | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| """authentik""" | ||||
| __version__ = "2021.6.2" | ||||
| __version__ = "2021.6.4" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
| @ -19,7 +19,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]: | ||||
|     auth_credentials = raw_header.decode() | ||||
|     if auth_credentials == "" or " " not in auth_credentials: | ||||
|         return None | ||||
|     auth_type, auth_credentials = auth_credentials.split() | ||||
|     auth_type, _, auth_credentials = auth_credentials.partition(" ") | ||||
|     if auth_type.lower() not in ["basic", "bearer"]: | ||||
|         LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower()) | ||||
|         raise AuthenticationFailed("Unsupported authentication type") | ||||
|  | ||||
| @ -2,12 +2,11 @@ | ||||
| from json import loads | ||||
|  | ||||
| from django.db.models.query import QuerySet | ||||
| from django.http.response import Http404 | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.http import urlencode | ||||
| from django_filters.filters import BooleanFilter, CharFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field | ||||
| from drf_spectacular.utils import extend_schema, extend_schema_field | ||||
| from guardian.utils import get_anonymous_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import CharField, JSONField, SerializerMethodField | ||||
| @ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|     @extend_schema( | ||||
|         responses={ | ||||
|             "200": LinkSerializer(many=False), | ||||
|             "404": OpenApiResponse(description="No recovery flow found."), | ||||
|             "404": LinkSerializer(many=False), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=True, pagination_class=None, filter_backends=[]) | ||||
| @ -184,7 +183,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         # Check that there is a recovery flow, if not return an error | ||||
|         flow = tenant.flow_recovery | ||||
|         if not flow: | ||||
|             raise Http404 | ||||
|             return Response({"link": ""}, status=404) | ||||
|         user: User = self.get_object() | ||||
|         token, __ = Token.objects.get_or_create( | ||||
|             identifier=f"{user.uid}-password-reset", | ||||
|  | ||||
| @ -14,7 +14,9 @@ def is_dict(value: Any): | ||||
|     """Ensure a value is a dictionary, useful for JSONFields""" | ||||
|     if isinstance(value, dict): | ||||
|         return | ||||
|     raise ValidationError("Value must be a dictionary.") | ||||
|     raise ValidationError( | ||||
|         "Value must be a dictionary, and not have any duplicate keys." | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class PassiveSerializer(Serializer): | ||||
|  | ||||
| @ -5,14 +5,13 @@ from typing import Any, Optional, Type | ||||
| from urllib.parse import urlencode | ||||
| from uuid import uuid4 | ||||
|  | ||||
| import django.db.models.options as options | ||||
| from deepmerge import always_merger | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import AbstractUser | ||||
| from django.contrib.auth.models import UserManager as DjangoUserManager | ||||
| from django.core import validators | ||||
| from django.db import models | ||||
| from django.db.models import Q, QuerySet | ||||
| from django.db.models import Q, QuerySet, options | ||||
| from django.http import HttpRequest | ||||
| from django.templatetags.static import static | ||||
| from django.utils.functional import cached_property | ||||
|  | ||||
| @ -213,7 +213,7 @@ class SourceFlowManager: | ||||
|         planner = FlowPlanner(flow) | ||||
|         plan = planner.plan(self.request, kwargs) | ||||
|         for stage in self.get_stages_to_append(flow): | ||||
|             plan.append(stage) | ||||
|             plan.append_stage(stage=stage) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| <script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script> | ||||
| <style> | ||||
| .pf-c-background-image::before { | ||||
|     background-image: url("{{ flow.background_url }}"); | ||||
|     --ak-flow-background: url("{{ flow.background_url }}"); | ||||
| } | ||||
| </style> | ||||
| {% endblock %} | ||||
|  | ||||
| @ -10,7 +10,7 @@ | ||||
| {% block head %} | ||||
| <style> | ||||
| .pf-c-background-image::before { | ||||
|     background-image: url("/static/dist/assets/images/flow_background.jpg"); | ||||
|     --ak-flow-background: url("/static/dist/assets/images/flow_background.jpg"); | ||||
| } | ||||
| </style> | ||||
| {% endblock %} | ||||
|  | ||||
| @ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer): | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "fingerprint", | ||||
|             "fingerprint_sha256", | ||||
|             "fingerprint_sha1", | ||||
|             "certificate_data", | ||||
|             "key_data", | ||||
|             "cert_expiry", | ||||
|  | ||||
| @ -16,11 +16,6 @@ from authentik.crypto.models import CertificateKeyPair | ||||
| class CertificateBuilder: | ||||
|     """Build self-signed certificates""" | ||||
|  | ||||
|     __public_key = None | ||||
|     __private_key = None | ||||
|     __builder = None | ||||
|     __certificate = None | ||||
|  | ||||
|     common_name: str | ||||
|  | ||||
|     def __init__(self): | ||||
|  | ||||
| @ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel): | ||||
|         return self._private_key | ||||
|  | ||||
|     @property | ||||
|     def fingerprint(self) -> str: | ||||
|     def fingerprint_sha256(self) -> str: | ||||
|         """Get SHA256 Fingerprint of certificate_data""" | ||||
|         return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode( | ||||
|             "utf-8" | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def fingerprint_sha1(self) -> str: | ||||
|         """Get SHA1 Fingerprint of certificate_data""" | ||||
|         return hexlify( | ||||
|             self.certificate.fingerprint(hashes.SHA1()), ":"  # nosec | ||||
|         ).decode("utf-8") | ||||
|  | ||||
|     @property | ||||
|     def kid(self): | ||||
|         """Get Key ID used for JWKS""" | ||||
|  | ||||
| @ -6,11 +6,11 @@ from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import OpenApiParameter, extend_schema | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import CharField, DictField, IntegerField | ||||
| from rest_framework.fields import DictField, IntegerField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer | ||||
| from authentik.events.models import Event, EventAction | ||||
| @ -19,11 +19,6 @@ from authentik.events.models import Event, EventAction | ||||
| class EventSerializer(ModelSerializer): | ||||
|     """Event Serializer""" | ||||
|  | ||||
|     # Since we only use this serializer for read-only operations, | ||||
|     # no checking of the action is done here. | ||||
|     # This allows clients to check wildcards, prefixes and custom types | ||||
|     action = CharField() | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = Event | ||||
| @ -96,7 +91,7 @@ class EventsFilter(django_filters.FilterSet): | ||||
|         fields = ["action", "client_ip", "username"] | ||||
|  | ||||
|  | ||||
| class EventViewSet(ReadOnlyModelViewSet): | ||||
| class EventViewSet(ModelViewSet): | ||||
|     """Event Read-Only Viewset""" | ||||
|  | ||||
|     queryset = Event.objects.all() | ||||
|  | ||||
| @ -46,7 +46,7 @@ class NotificationTransportTestSerializer(Serializer): | ||||
|  | ||||
|     messages = ListField(child=CharField()) | ||||
|  | ||||
|     def create(self, request: Request) -> Response: | ||||
|     def create(self, validated_data: Request) -> Response: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|  | ||||
| @ -27,10 +27,9 @@ class GeoIPDict(TypedDict): | ||||
| class GeoIPReader: | ||||
|     """Slim wrapper around GeoIP API""" | ||||
|  | ||||
|     __reader: Optional[Reader] = None | ||||
|     __last_mtime: float = 0.0 | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.__reader: Optional[Reader] = None | ||||
|         self.__last_mtime: float = 0.0 | ||||
|         self.__open() | ||||
|  | ||||
|     def __open(self): | ||||
|  | ||||
| @ -3,6 +3,7 @@ from functools import partial | ||||
| from typing import Callable | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import SuspiciousOperation | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import post_save, pre_delete | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| @ -13,6 +14,7 @@ from authentik.core.models import User | ||||
| from authentik.events.models import Event, EventAction, Notification | ||||
| from authentik.events.signals import EventNewThread | ||||
| from authentik.events.utils import model_to_dict | ||||
| from authentik.lib.sentry import before_send | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
|  | ||||
|  | ||||
| @ -62,12 +64,21 @@ class AuditMiddleware: | ||||
|  | ||||
|         if settings.DEBUG: | ||||
|             return | ||||
|         thread = EventNewThread( | ||||
|             EventAction.SYSTEM_EXCEPTION, | ||||
|             request, | ||||
|             message=exception_to_string(exception), | ||||
|         ) | ||||
|         thread.run() | ||||
|         # Special case for SuspiciousOperation, we have a special event action for that | ||||
|         if isinstance(exception, SuspiciousOperation): | ||||
|             thread = EventNewThread( | ||||
|                 EventAction.SUSPICIOUS_REQUEST, | ||||
|                 request, | ||||
|                 message=str(exception), | ||||
|             ) | ||||
|             thread.run() | ||||
|         elif before_send({}, {"exc_info": (None, exception, None)}) is not None: | ||||
|             thread = EventNewThread( | ||||
|                 EventAction.SYSTEM_EXCEPTION, | ||||
|                 request, | ||||
|                 message=exception_to_string(exception), | ||||
|             ) | ||||
|             thread.run() | ||||
|  | ||||
|     @staticmethod | ||||
|     # pylint: disable=unused-argument | ||||
|  | ||||
| @ -105,7 +105,11 @@ def notification_transport( | ||||
|     """Send notification over specified transport""" | ||||
|     self.save_on_success = False | ||||
|     try: | ||||
|         notification: Notification = Notification.objects.get(pk=notification_pk) | ||||
|         notification: Notification = Notification.objects.filter( | ||||
|             pk=notification_pk | ||||
|         ).first() | ||||
|         if not notification: | ||||
|             return | ||||
|         transport: NotificationTransport = NotificationTransport.objects.get( | ||||
|             pk=transport_pk | ||||
|         ) | ||||
|  | ||||
| @ -25,6 +25,7 @@ class FlowStageBindingSerializer(ModelSerializer): | ||||
|             "re_evaluate_policies", | ||||
|             "order", | ||||
|             "policy_engine_mode", | ||||
|             "invalid_response_action", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -5,8 +5,7 @@ from typing import TYPE_CHECKING, Optional | ||||
| from django.http.request import HttpRequest | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.flows.models import Stage | ||||
| from authentik.flows.models import FlowStageBinding | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.models import PolicyBinding | ||||
|  | ||||
| @ -22,11 +21,14 @@ class StageMarker: | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def process( | ||||
|         self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest] | ||||
|     ) -> Optional[Stage]: | ||||
|         self, | ||||
|         plan: "FlowPlan", | ||||
|         binding: FlowStageBinding, | ||||
|         http_request: HttpRequest, | ||||
|     ) -> Optional[FlowStageBinding]: | ||||
|         """Process callback for this marker. This should be overridden by sub-classes. | ||||
|         If a stage should be removed, return None.""" | ||||
|         return stage | ||||
|         return binding | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| @ -34,24 +36,34 @@ class ReevaluateMarker(StageMarker): | ||||
|     """Reevaluate Marker, forces stage's policies to be evaluated again.""" | ||||
|  | ||||
|     binding: PolicyBinding | ||||
|     user: User | ||||
|  | ||||
|     def process( | ||||
|         self, plan: "FlowPlan", stage: Stage, http_request: Optional[HttpRequest] | ||||
|     ) -> Optional[Stage]: | ||||
|         self, | ||||
|         plan: "FlowPlan", | ||||
|         binding: FlowStageBinding, | ||||
|         http_request: HttpRequest, | ||||
|     ) -> Optional[FlowStageBinding]: | ||||
|         """Re-evaluate policies bound to stage, and if they fail, remove from plan""" | ||||
|         engine = PolicyEngine(self.binding, self.user) | ||||
|         from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
|  | ||||
|         LOGGER.debug( | ||||
|             "f(plan_inst)[re-eval marker]: running re-evaluation", | ||||
|             binding=binding, | ||||
|             policy_binding=self.binding, | ||||
|         ) | ||||
|         engine = PolicyEngine( | ||||
|             self.binding, plan.context.get(PLAN_CONTEXT_PENDING_USER, http_request.user) | ||||
|         ) | ||||
|         engine.use_cache = False | ||||
|         if http_request: | ||||
|             engine.request.set_http_request(http_request) | ||||
|         engine.request.set_http_request(http_request) | ||||
|         engine.request.context = plan.context | ||||
|         engine.build() | ||||
|         result = engine.result | ||||
|         if result.passing: | ||||
|             return stage | ||||
|             return binding | ||||
|         LOGGER.warning( | ||||
|             "f(plan_inst)[re-eval marker]: stage failed re-evaluation", | ||||
|             stage=stage, | ||||
|             "f(plan_inst)[re-eval marker]: binding failed re-evaluation", | ||||
|             binding=binding, | ||||
|             messages=result.messages, | ||||
|         ) | ||||
|         return None | ||||
|  | ||||
| @ -135,7 +135,7 @@ class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_flows", "0017_auto_20210329_1334"), | ||||
|         ("authentik_stages_user_write", "__latest__"), | ||||
|         ("authentik_stages_user_write", "0002_auto_20200918_1653"), | ||||
|         ("authentik_stages_user_login", "__latest__"), | ||||
|         ("authentik_stages_password", "0002_passwordstage_change_flow"), | ||||
|         ("authentik_policies", "0001_initial"), | ||||
|  | ||||
| @ -0,0 +1,22 @@ | ||||
| # Generated by Django 3.2.4 on 2021-06-27 16:20 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_flows", "0020_flow_compatibility_mode"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="flowstagebinding", | ||||
|             name="invalid_response_action", | ||||
|             field=models.TextField( | ||||
|                 choices=[("retry", "Retry"), ("continue", "Continue")], | ||||
|                 default="retry", | ||||
|                 help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor while CONTINUE continues with the next stage.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,26 @@ | ||||
| # Generated by Django 3.2.4 on 2021-07-03 13:13 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_flows", "0021_flowstagebinding_invalid_response_action"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="flowstagebinding", | ||||
|             name="invalid_response_action", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("retry", "Retry"), | ||||
|                     ("restart", "Restart"), | ||||
|                     ("restart_with_context", "Restart With Context"), | ||||
|                 ], | ||||
|                 default="retry", | ||||
|                 help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -27,6 +27,14 @@ class NotConfiguredAction(models.TextChoices): | ||||
|     CONFIGURE = "configure" | ||||
|  | ||||
|  | ||||
| class InvalidResponseAction(models.TextChoices): | ||||
|     """Configure how the flow executor should handle invalid responses to challenges""" | ||||
|  | ||||
|     RETRY = "retry" | ||||
|     RESTART = "restart" | ||||
|     RESTART_WITH_CONTEXT = "restart_with_context" | ||||
|  | ||||
|  | ||||
| class FlowDesignation(models.TextChoices): | ||||
|     """Designation of what a Flow should be used for. At a later point, this | ||||
|     should be replaced by a database entry.""" | ||||
| @ -201,6 +209,17 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel): | ||||
|         help_text=_("Evaluate policies when the Stage is present to the user."), | ||||
|     ) | ||||
|  | ||||
|     invalid_response_action = models.TextField( | ||||
|         choices=InvalidResponseAction.choices, | ||||
|         default=InvalidResponseAction.RETRY, | ||||
|         help_text=_( | ||||
|             "Configure how the flow executor should handle an invalid response to a " | ||||
|             "challenge. RETRY returns the error message and a similar challenge to the " | ||||
|             "executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT " | ||||
|             "restarts the flow while keeping the current context." | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     order = models.IntegerField() | ||||
|  | ||||
|     objects = InheritanceManager() | ||||
|  | ||||
| @ -52,33 +52,41 @@ class FlowPlan: | ||||
|  | ||||
|     flow_pk: str | ||||
|  | ||||
|     stages: list[Stage] = field(default_factory=list) | ||||
|     bindings: list[FlowStageBinding] = field(default_factory=list) | ||||
|     context: dict[str, Any] = field(default_factory=dict) | ||||
|     markers: list[StageMarker] = field(default_factory=list) | ||||
|  | ||||
|     def append(self, stage: Stage, marker: Optional[StageMarker] = None): | ||||
|     def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None): | ||||
|         """Append `stage` to all stages, optionall with stage marker""" | ||||
|         self.stages.append(stage) | ||||
|         return self.append(FlowStageBinding(stage=stage), marker) | ||||
|  | ||||
|     def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None): | ||||
|         """Append `stage` to all stages, optionall with stage marker""" | ||||
|         self.bindings.append(binding) | ||||
|         self.markers.append(marker or StageMarker()) | ||||
|  | ||||
|     def insert(self, stage: Stage, marker: Optional[StageMarker] = None): | ||||
|     def insert_stage(self, stage: Stage, marker: Optional[StageMarker] = None): | ||||
|         """Insert stage into plan, as immediate next stage""" | ||||
|         self.stages.insert(1, stage) | ||||
|         self.bindings.insert(1, FlowStageBinding(stage=stage, order=0)) | ||||
|         self.markers.insert(1, marker or StageMarker()) | ||||
|  | ||||
|     def next(self, http_request: Optional[HttpRequest]) -> Optional[Stage]: | ||||
|     def next(self, http_request: Optional[HttpRequest]) -> Optional[FlowStageBinding]: | ||||
|         """Return next pending stage from the bottom of the list""" | ||||
|         if not self.has_stages: | ||||
|             return None | ||||
|         stage = self.stages[0] | ||||
|         binding = self.bindings[0] | ||||
|         marker = self.markers[0] | ||||
|  | ||||
|         if marker.__class__ is not StageMarker: | ||||
|             LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker) | ||||
|         marked_stage = marker.process(self, stage, http_request) | ||||
|             LOGGER.debug( | ||||
|                 "f(plan_inst): stage has marker", binding=binding, marker=marker | ||||
|             ) | ||||
|         marked_stage = marker.process(self, binding, http_request) | ||||
|         if not marked_stage: | ||||
|             LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage) | ||||
|             self.stages.remove(stage) | ||||
|             LOGGER.debug( | ||||
|                 "f(plan_inst): marker returned none, next stage", binding=binding | ||||
|             ) | ||||
|             self.bindings.remove(binding) | ||||
|             self.markers.remove(marker) | ||||
|             if not self.has_stages: | ||||
|                 return None | ||||
| @ -89,12 +97,12 @@ class FlowPlan: | ||||
|     def pop(self): | ||||
|         """Pop next pending stage from bottom of list""" | ||||
|         self.markers.pop(0) | ||||
|         self.stages.pop(0) | ||||
|         self.bindings.pop(0) | ||||
|  | ||||
|     @property | ||||
|     def has_stages(self) -> bool: | ||||
|         """Check if there are any stages left in this plan""" | ||||
|         return len(self.markers) + len(self.stages) > 0 | ||||
|         return len(self.markers) + len(self.bindings) > 0 | ||||
|  | ||||
|  | ||||
| class FlowPlanner: | ||||
| @ -161,7 +169,7 @@ class FlowPlanner: | ||||
|             plan = self._build_plan(user, request, default_context) | ||||
|             cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) | ||||
|             GAUGE_FLOWS_CACHED.update() | ||||
|             if not plan.stages and not self.allow_empty_flows: | ||||
|             if not plan.bindings and not self.allow_empty_flows: | ||||
|                 raise EmptyFlowException() | ||||
|             return plan | ||||
|  | ||||
| @ -216,9 +224,9 @@ class FlowPlanner: | ||||
|                         "f(plan): stage has re-evaluate marker", | ||||
|                         stage=binding.stage, | ||||
|                     ) | ||||
|                     marker = ReevaluateMarker(binding=binding, user=user) | ||||
|                     marker = ReevaluateMarker(binding=binding) | ||||
|                 if stage: | ||||
|                     plan.append(stage, marker) | ||||
|                     plan.append(binding, marker) | ||||
|             HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug) | ||||
|         self._logger.debug( | ||||
|             "f(plan): finished building", | ||||
|  | ||||
| @ -16,6 +16,7 @@ from authentik.flows.challenge import ( | ||||
|     HttpChallengeResponse, | ||||
|     WithUserInfoChallenge, | ||||
| ) | ||||
| from authentik.flows.models import InvalidResponseAction | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||
| from authentik.flows.views import FlowExecutorView | ||||
|  | ||||
| @ -69,7 +70,13 @@ class ChallengeStageView(StageView): | ||||
|         """Return a challenge for the frontend to solve""" | ||||
|         challenge = self._get_challenge(*args, **kwargs) | ||||
|         if not challenge.is_valid(): | ||||
|             LOGGER.warning(challenge.errors, stage_view=self, challenge=challenge) | ||||
|             LOGGER.warning( | ||||
|                 "f(ch): Invalid challenge", | ||||
|                 binding=self.executor.current_binding, | ||||
|                 errors=challenge.errors, | ||||
|                 stage_view=self, | ||||
|                 challenge=challenge, | ||||
|             ) | ||||
|         return HttpChallengeResponse(challenge) | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
| @ -77,6 +84,21 @@ class ChallengeStageView(StageView): | ||||
|         """Handle challenge response""" | ||||
|         challenge: ChallengeResponse = self.get_response_instance(data=request.data) | ||||
|         if not challenge.is_valid(): | ||||
|             if self.executor.current_binding.invalid_response_action in [ | ||||
|                 InvalidResponseAction.RESTART, | ||||
|                 InvalidResponseAction.RESTART_WITH_CONTEXT, | ||||
|             ]: | ||||
|                 keep_context = ( | ||||
|                     self.executor.current_binding.invalid_response_action | ||||
|                     == InvalidResponseAction.RESTART_WITH_CONTEXT | ||||
|                 ) | ||||
|                 LOGGER.debug( | ||||
|                     "f(ch): Invalid response, restarting flow", | ||||
|                     binding=self.executor.current_binding, | ||||
|                     stage_view=self, | ||||
|                     keep_context=keep_context, | ||||
|                 ) | ||||
|                 return self.executor.restart_flow(keep_context) | ||||
|             return self.challenge_invalid(challenge) | ||||
|         return self.challenge_valid(challenge) | ||||
|  | ||||
| @ -126,5 +148,10 @@ class ChallengeStageView(StageView): | ||||
|                 ) | ||||
|         challenge_response.initial_data["response_errors"] = full_errors | ||||
|         if not challenge_response.is_valid(): | ||||
|             LOGGER.warning(challenge_response.errors) | ||||
|             LOGGER.warning( | ||||
|                 "f(ch): invalid challenge response", | ||||
|                 binding=self.executor.current_binding, | ||||
|                 errors=challenge_response.errors, | ||||
|                 stage_view=self, | ||||
|             ) | ||||
|         return HttpChallengeResponse(challenge_response) | ||||
|  | ||||
| @ -182,8 +182,8 @@ class TestFlowPlanner(TestCase): | ||||
|             planner = FlowPlanner(flow) | ||||
|             plan = planner.plan(request) | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding.stage) | ||||
|             self.assertEqual(plan.stages[1], binding2.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding) | ||||
|             self.assertEqual(plan.bindings[1], binding2) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], ReevaluateMarker) | ||||
|  | ||||
| @ -11,15 +11,23 @@ from authentik.core.models import User | ||||
| from authentik.flows.challenge import ChallengeTypes | ||||
| from authentik.flows.exceptions import FlowNonApplicableException | ||||
| from authentik.flows.markers import ReevaluateMarker, StageMarker | ||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||
| from authentik.flows.models import ( | ||||
|     Flow, | ||||
|     FlowDesignation, | ||||
|     FlowStageBinding, | ||||
|     InvalidResponseAction, | ||||
| ) | ||||
| from authentik.flows.planner import FlowPlan, FlowPlanner | ||||
| from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView | ||||
| from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.policies.reputation.models import ReputationPolicy | ||||
| from authentik.policies.types import PolicyResult | ||||
| from authentik.stages.deny.models import DenyStage | ||||
| from authentik.stages.dummy.models import DummyStage | ||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | ||||
|  | ||||
| POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) | ||||
| POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) | ||||
| @ -52,8 +60,9 @@ class TestFlowExecutor(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         stage = DummyStage.objects.create(name="dummy") | ||||
|         binding = FlowStageBinding(target=flow, stage=stage, order=0) | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()] | ||||
|             flow_pk=flow.pk.hex + "a", bindings=[binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -163,7 +172,7 @@ class TestFlowExecutor(TestCase): | ||||
|         # Check that two stages are in plan | ||||
|         session = self.client.session | ||||
|         plan: FlowPlan = session[SESSION_KEY_PLAN] | ||||
|         self.assertEqual(len(plan.stages), 2) | ||||
|         self.assertEqual(len(plan.bindings), 2) | ||||
|         # Second request, submit form, one stage left | ||||
|         response = self.client.post(exec_url) | ||||
|         # Second request redirects to the same URL | ||||
| @ -172,7 +181,7 @@ class TestFlowExecutor(TestCase): | ||||
|         # Check that two stages are in plan | ||||
|         session = self.client.session | ||||
|         plan: FlowPlan = session[SESSION_KEY_PLAN] | ||||
|         self.assertEqual(len(plan.stages), 1) | ||||
|         self.assertEqual(len(plan.bindings), 1) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.to_stage_response", | ||||
| @ -213,8 +222,8 @@ class TestFlowExecutor(TestCase): | ||||
|  | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding.stage) | ||||
|             self.assertEqual(plan.stages[1], binding2.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding) | ||||
|             self.assertEqual(plan.bindings[1], binding2) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], ReevaluateMarker) | ||||
| @ -267,9 +276,9 @@ class TestFlowExecutor(TestCase): | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding.stage) | ||||
|             self.assertEqual(plan.stages[1], binding2.stage) | ||||
|             self.assertEqual(plan.stages[2], binding3.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding) | ||||
|             self.assertEqual(plan.bindings[1], binding2) | ||||
|             self.assertEqual(plan.bindings[2], binding3) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], ReevaluateMarker) | ||||
| @ -281,8 +290,8 @@ class TestFlowExecutor(TestCase): | ||||
|  | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding2.stage) | ||||
|             self.assertEqual(plan.stages[1], binding3.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding2) | ||||
|             self.assertEqual(plan.bindings[1], binding3) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], StageMarker) | ||||
| @ -338,9 +347,9 @@ class TestFlowExecutor(TestCase): | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding.stage) | ||||
|             self.assertEqual(plan.stages[1], binding2.stage) | ||||
|             self.assertEqual(plan.stages[2], binding3.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding) | ||||
|             self.assertEqual(plan.bindings[1], binding2) | ||||
|             self.assertEqual(plan.bindings[2], binding3) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], ReevaluateMarker) | ||||
| @ -352,8 +361,8 @@ class TestFlowExecutor(TestCase): | ||||
|  | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding2.stage) | ||||
|             self.assertEqual(plan.stages[1], binding3.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding2) | ||||
|             self.assertEqual(plan.bindings[1], binding3) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], StageMarker) | ||||
| @ -364,7 +373,7 @@ class TestFlowExecutor(TestCase): | ||||
|  | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding3.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding3) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|  | ||||
| @ -438,10 +447,10 @@ class TestFlowExecutor(TestCase): | ||||
|  | ||||
|             plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] | ||||
|  | ||||
|             self.assertEqual(plan.stages[0], binding.stage) | ||||
|             self.assertEqual(plan.stages[1], binding2.stage) | ||||
|             self.assertEqual(plan.stages[2], binding3.stage) | ||||
|             self.assertEqual(plan.stages[3], binding4.stage) | ||||
|             self.assertEqual(plan.bindings[0], binding) | ||||
|             self.assertEqual(plan.bindings[1], binding2) | ||||
|             self.assertEqual(plan.bindings[2], binding3) | ||||
|             self.assertEqual(plan.bindings[3], binding4) | ||||
|  | ||||
|             self.assertIsInstance(plan.markers[0], StageMarker) | ||||
|             self.assertIsInstance(plan.markers[1], ReevaluateMarker) | ||||
| @ -512,3 +521,78 @@ class TestFlowExecutor(TestCase): | ||||
|  | ||||
|         stage_view = StageView(executor) | ||||
|         self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username) | ||||
|  | ||||
|     def test_invalid_restart(self): | ||||
|         """Test flow that restarts on invalid entry""" | ||||
|         flow = Flow.objects.create( | ||||
|             name="restart-on-invalid", | ||||
|             slug="restart-on-invalid", | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         # Stage 0 is a deny stage that is added dynamically | ||||
|         # when the reputation policy says so | ||||
|         deny_stage = DenyStage.objects.create(name="deny") | ||||
|         reputation_policy = ReputationPolicy.objects.create( | ||||
|             name="reputation", threshold=-1, check_ip=False | ||||
|         ) | ||||
|         deny_binding = FlowStageBinding.objects.create( | ||||
|             target=flow, | ||||
|             stage=deny_stage, | ||||
|             order=0, | ||||
|             evaluate_on_plan=False, | ||||
|             re_evaluate_policies=True, | ||||
|         ) | ||||
|         PolicyBinding.objects.create( | ||||
|             policy=reputation_policy, target=deny_binding, order=0 | ||||
|         ) | ||||
|  | ||||
|         # Stage 1 is an identification stage | ||||
|         ident_stage = IdentificationStage.objects.create( | ||||
|             name="ident", | ||||
|             user_fields=[UserFields.E_MAIL], | ||||
|         ) | ||||
|         FlowStageBinding.objects.create( | ||||
|             target=flow, | ||||
|             stage=ident_stage, | ||||
|             order=1, | ||||
|             invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT, | ||||
|         ) | ||||
|         exec_url = reverse( | ||||
|             "authentik_api:flow-executor", kwargs={"flow_slug": flow.slug} | ||||
|         ) | ||||
|         # First request, run the planner | ||||
|         response = self.client.get(exec_url) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual( | ||||
|             force_str(response.content), | ||||
|             { | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|                 "component": "ak-stage-identification", | ||||
|                 "flow_info": { | ||||
|                     "background": flow.background_url, | ||||
|                     "cancel_url": reverse("authentik_flows:cancel"), | ||||
|                     "title": "", | ||||
|                 }, | ||||
|                 "password_fields": False, | ||||
|                 "primary_action": "Log in", | ||||
|                 "sources": [], | ||||
|                 "user_fields": [UserFields.E_MAIL], | ||||
|             }, | ||||
|         ) | ||||
|         response = self.client.post( | ||||
|             exec_url, {"uid_field": "invalid-string"}, follow=True | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual( | ||||
|             force_str(response.content), | ||||
|             { | ||||
|                 "component": "ak-stage-access-denied", | ||||
|                 "error_message": None, | ||||
|                 "flow_info": { | ||||
|                     "background": flow.background_url, | ||||
|                     "cancel_url": reverse("authentik_flows:cancel"), | ||||
|                     "title": "", | ||||
|                 }, | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @ -40,15 +40,11 @@ def transaction_rollback(): | ||||
| class FlowImporter: | ||||
|     """Import Flow from json""" | ||||
|  | ||||
|     __import: FlowBundle | ||||
|  | ||||
|     __pk_map: dict[Any, Model] | ||||
|  | ||||
|     logger: BoundLogger | ||||
|  | ||||
|     def __init__(self, json_input: str): | ||||
|         self.__pk_map: dict[Any, Model] = {} | ||||
|         self.logger = get_logger() | ||||
|         self.__pk_map = {} | ||||
|         import_dict = loads(json_input) | ||||
|         try: | ||||
|             self.__import = from_dict(FlowBundle, import_dict) | ||||
|  | ||||
| @ -4,6 +4,7 @@ from typing import Any, Optional | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.core.cache import cache | ||||
| from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect | ||||
| from django.http.request import QueryDict | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| @ -37,7 +38,13 @@ from authentik.flows.challenge import ( | ||||
|     WithUserInfoChallenge, | ||||
| ) | ||||
| from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage | ||||
| from authentik.flows.models import ( | ||||
|     ConfigurableStage, | ||||
|     Flow, | ||||
|     FlowDesignation, | ||||
|     FlowStageBinding, | ||||
|     Stage, | ||||
| ) | ||||
| from authentik.flows.planner import ( | ||||
|     PLAN_CONTEXT_PENDING_USER, | ||||
|     PLAN_CONTEXT_REDIRECT, | ||||
| @ -107,6 +114,7 @@ class FlowExecutorView(APIView): | ||||
|     flow: Flow | ||||
|  | ||||
|     plan: Optional[FlowPlan] = None | ||||
|     current_binding: FlowStageBinding | ||||
|     current_stage: Stage | ||||
|     current_stage_view: View | ||||
|  | ||||
| @ -126,7 +134,7 @@ class FlowExecutorView(APIView): | ||||
|         message = exc.__doc__ if exc.__doc__ else str(exc) | ||||
|         return self.stage_invalid(error_message=message) | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     # pylint: disable=unused-argument, too-many-return-statements | ||||
|     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: | ||||
|         # Early check if theres an active Plan for the current session | ||||
|         if SESSION_KEY_PLAN in self.request.session: | ||||
| @ -159,11 +167,23 @@ class FlowExecutorView(APIView): | ||||
|         request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", "")) | ||||
|         # We don't save the Plan after getting the next stage | ||||
|         # as it hasn't been successfully passed yet | ||||
|         next_stage = self.plan.next(self.request) | ||||
|         if not next_stage: | ||||
|         try: | ||||
|             # This is the first time we actually access any attribute on the selected plan | ||||
|             # if the cached plan is from an older version, it might have different attributes | ||||
|             # in which case we just delete the plan and invalidate everything | ||||
|             next_binding = self.plan.next(self.request) | ||||
|         except Exception as exc:  # pylint: disable=broad-except | ||||
|             self._logger.warning( | ||||
|                 "f(exec): found incompatible flow plan, invalidating run", exc=exc | ||||
|             ) | ||||
|             keys = cache.keys("flow_*") | ||||
|             cache.delete_many(keys) | ||||
|             return self.stage_invalid() | ||||
|         if not next_binding: | ||||
|             self._logger.debug("f(exec): no more stages, flow is done.") | ||||
|             return self._flow_done() | ||||
|         self.current_stage = next_stage | ||||
|         self.current_binding = next_binding | ||||
|         self.current_stage = next_binding.stage | ||||
|         self._logger.debug( | ||||
|             "f(exec): Current stage", | ||||
|             current_stage=self.current_stage, | ||||
| @ -268,8 +288,31 @@ class FlowExecutorView(APIView): | ||||
|         planner = FlowPlanner(self.flow) | ||||
|         plan = planner.plan(self.request) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         try: | ||||
|             # Call the has_stages getter to check that | ||||
|             # there are no issues with the class we might've gotten | ||||
|             # from the cache. If there are errors, just delete all cached flows | ||||
|             _ = plan.has_stages | ||||
|         except Exception:  # pylint: disable=broad-except | ||||
|             keys = cache.keys("flow_*") | ||||
|             cache.delete_many(keys) | ||||
|             return self._initiate_plan() | ||||
|         return plan | ||||
|  | ||||
|     def restart_flow(self, keep_context=False) -> HttpResponse: | ||||
|         """Restart the currently active flow, optionally keeping the current context""" | ||||
|         planner = FlowPlanner(self.flow) | ||||
|         default_context = None | ||||
|         if keep_context: | ||||
|             default_context = self.plan.context | ||||
|         plan = planner.plan(self.request, default_context) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         kwargs = self.kwargs | ||||
|         kwargs.update({"flow_slug": self.flow.slug}) | ||||
|         return redirect_with_qs( | ||||
|             "authentik_api:flow-executor", self.request.GET, **kwargs | ||||
|         ) | ||||
|  | ||||
|     def _flow_done(self) -> HttpResponse: | ||||
|         """User Successfully passed all stages""" | ||||
|         # Since this is wrapped by the ExecutorShell, the next argument is saved in the session | ||||
| @ -293,10 +336,10 @@ class FlowExecutorView(APIView): | ||||
|         ) | ||||
|         self.plan.pop() | ||||
|         self.request.session[SESSION_KEY_PLAN] = self.plan | ||||
|         if self.plan.stages: | ||||
|         if self.plan.bindings: | ||||
|             self._logger.debug( | ||||
|                 "f(exec): Continuing with next stage", | ||||
|                 remaining=len(self.plan.stages), | ||||
|                 remaining=len(self.plan.bindings), | ||||
|             ) | ||||
|             kwargs = self.kwargs | ||||
|             kwargs.update({"flow_slug": self.flow.slug}) | ||||
|  | ||||
| @ -26,10 +26,9 @@ class ConfigLoader: | ||||
|  | ||||
|     loaded_file = [] | ||||
|  | ||||
|     __config = {} | ||||
|  | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.__config = {} | ||||
|         base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../..")) | ||||
|         for path in SEARCH_PATHS: | ||||
|             # Check if path is relative, and if so join with base_dir | ||||
|  | ||||
| @ -3,6 +3,7 @@ import re | ||||
| from textwrap import indent | ||||
| from typing import Any, Iterable, Optional | ||||
|  | ||||
| from django.core.exceptions import FieldError | ||||
| from requests import Session | ||||
| from rest_framework.serializers import ValidationError | ||||
| from sentry_sdk.hub import Hub | ||||
| @ -29,10 +30,10 @@ class BaseEvaluator: | ||||
|         # update website/docs/expressions/_objects.md | ||||
|         # update website/docs/expressions/_functions.md | ||||
|         self._globals = { | ||||
|             "regex_match": BaseEvaluator.expr_filter_regex_match, | ||||
|             "regex_replace": BaseEvaluator.expr_filter_regex_replace, | ||||
|             "ak_is_group_member": BaseEvaluator.expr_func_is_group_member, | ||||
|             "ak_user_by": BaseEvaluator.expr_func_user_by, | ||||
|             "regex_match": BaseEvaluator.expr_regex_match, | ||||
|             "regex_replace": BaseEvaluator.expr_regex_replace, | ||||
|             "ak_is_group_member": BaseEvaluator.expr_is_group_member, | ||||
|             "ak_user_by": BaseEvaluator.expr_user_by, | ||||
|             "ak_logger": get_logger(), | ||||
|             "requests": Session(), | ||||
|         } | ||||
| @ -40,25 +41,28 @@ class BaseEvaluator: | ||||
|         self._filename = "BaseEvalautor" | ||||
|  | ||||
|     @staticmethod | ||||
|     def expr_filter_regex_match(value: Any, regex: str) -> bool: | ||||
|     def expr_regex_match(value: Any, regex: str) -> bool: | ||||
|         """Expression Filter to run re.search""" | ||||
|         return re.search(regex, value) is None | ||||
|         return re.search(regex, value) is not None | ||||
|  | ||||
|     @staticmethod | ||||
|     def expr_filter_regex_replace(value: Any, regex: str, repl: str) -> str: | ||||
|     def expr_regex_replace(value: Any, regex: str, repl: str) -> str: | ||||
|         """Expression Filter to run re.sub""" | ||||
|         return re.sub(regex, repl, value) | ||||
|  | ||||
|     @staticmethod | ||||
|     def expr_func_user_by(**filters) -> Optional[User]: | ||||
|     def expr_user_by(**filters) -> Optional[User]: | ||||
|         """Get user by filters""" | ||||
|         users = User.objects.filter(**filters) | ||||
|         if users: | ||||
|             return users.first() | ||||
|         return None | ||||
|         try: | ||||
|             users = User.objects.filter(**filters) | ||||
|             if users: | ||||
|                 return users.first() | ||||
|             return None | ||||
|         except FieldError: | ||||
|             return None | ||||
|  | ||||
|     @staticmethod | ||||
|     def expr_func_is_group_member(user: User, **group_filters) -> bool: | ||||
|     def expr_is_group_member(user: User, **group_filters) -> bool: | ||||
|         """Check if `user` is member of group with name `group_name`""" | ||||
|         return user.ak_groups.filter(**group_filters).exists() | ||||
|  | ||||
|  | ||||
							
								
								
									
										32
									
								
								authentik/lib/tests/test_evaluator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								authentik/lib/tests/test_evaluator.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| """Test Evaluator base functions""" | ||||
| from django.test import TestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.lib.expression.evaluator import BaseEvaluator | ||||
|  | ||||
|  | ||||
| class TestEvaluator(TestCase): | ||||
|     """Test Evaluator base functions""" | ||||
|  | ||||
|     def test_regex_match(self): | ||||
|         """Test expr_regex_match""" | ||||
|         self.assertFalse(BaseEvaluator.expr_regex_match("foo", "bar")) | ||||
|         self.assertTrue(BaseEvaluator.expr_regex_match("foo", "foo")) | ||||
|  | ||||
|     def test_regex_replace(self): | ||||
|         """Test expr_regex_replace""" | ||||
|         self.assertEqual(BaseEvaluator.expr_regex_replace("foo", "o", "a"), "faa") | ||||
|  | ||||
|     def test_user_by(self): | ||||
|         """Test expr_user_by""" | ||||
|         self.assertIsNotNone(BaseEvaluator.expr_user_by(username="akadmin")) | ||||
|         self.assertIsNone(BaseEvaluator.expr_user_by(username="bar")) | ||||
|         self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar")) | ||||
|  | ||||
|     def test_is_group_member(self): | ||||
|         """Test expr_is_group_member""" | ||||
|         self.assertFalse( | ||||
|             BaseEvaluator.expr_is_group_member( | ||||
|                 User.objects.get(username="akadmin"), name="test" | ||||
|             ) | ||||
|         ) | ||||
| @ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer): | ||||
|                 raise ValidationError( | ||||
|                     ( | ||||
|                         f"Outpost type {self.initial_data['type']} can't be used with " | ||||
|                         f"{type(provider)} providers." | ||||
|                         f"{provider.__class__.__name__} providers." | ||||
|                     ) | ||||
|                 ) | ||||
|         return providers | ||||
|  | ||||
| @ -69,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer): | ||||
|         self.last_uid = self.channel_name | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def disconnect(self, close_code): | ||||
|     def disconnect(self, code): | ||||
|         if self.outpost and self.last_uid: | ||||
|             state = OutpostState.for_instance_uid(self.outpost, self.last_uid) | ||||
|             if self.channel_name in state.channel_ids: | ||||
|  | ||||
| @ -36,8 +36,10 @@ class DockerController(BaseController): | ||||
|  | ||||
|     def _get_env(self) -> dict[str, str]: | ||||
|         return { | ||||
|             "AUTHENTIK_HOST": self.outpost.config.authentik_host, | ||||
|             "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure), | ||||
|             "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(), | ||||
|             "AUTHENTIK_INSECURE": str( | ||||
|                 self.outpost.config.authentik_host_insecure | ||||
|             ).lower(), | ||||
|             "AUTHENTIK_TOKEN": self.outpost.token.key, | ||||
|         } | ||||
|  | ||||
| @ -45,11 +47,34 @@ class DockerController(BaseController): | ||||
|         """Check if container's env is equal to what we would set. Return true if container needs | ||||
|         to be rebuilt.""" | ||||
|         should_be = self._get_env() | ||||
|         container_env = container.attrs.get("Config", {}).get("Env", {}) | ||||
|         container_env = container.attrs.get("Config", {}).get("Env", []) | ||||
|         for key, expected_value in should_be.items(): | ||||
|             if key not in container_env: | ||||
|                 continue | ||||
|             if container_env[key] != expected_value: | ||||
|             entry = f"{key.upper()}={expected_value}" | ||||
|             if entry not in container_env: | ||||
|                 return True | ||||
|         return False | ||||
|  | ||||
|     def _comp_ports(self, container: Container) -> bool: | ||||
|         """Check that the container has the correct ports exposed. Return true if container needs | ||||
|         to be rebuilt.""" | ||||
|         # with TEST enabled, we use host-network | ||||
|         if settings.TEST: | ||||
|             return False | ||||
|         # When the container isn't running, the API doesn't report any port mappings | ||||
|         if container.status != "running": | ||||
|             return False | ||||
|         # {'3389/tcp': [ | ||||
|         #   {'HostIp': '0.0.0.0', 'HostPort': '389'}, | ||||
|         #   {'HostIp': '::', 'HostPort': '389'} | ||||
|         # ]} | ||||
|         for port in self.deployment_ports: | ||||
|             key = f"{port.inner_port or port.port}/{port.protocol.lower()}" | ||||
|             if key not in container.ports: | ||||
|                 return True | ||||
|             host_matching = False | ||||
|             for host_port in container.ports[key]: | ||||
|                 host_matching = host_port.get("HostPort") == str(port.port) | ||||
|             if not host_matching: | ||||
|                 return True | ||||
|         return False | ||||
|  | ||||
| @ -58,7 +83,7 @@ class DockerController(BaseController): | ||||
|         try: | ||||
|             return self.client.containers.get(container_name), False | ||||
|         except NotFound: | ||||
|             self.logger.info("Container does not exist, creating") | ||||
|             self.logger.info("(Re-)creating container...") | ||||
|             image_name = self.get_container_image() | ||||
|             self.client.images.pull(image_name) | ||||
|             container_args = { | ||||
| @ -86,6 +111,7 @@ class DockerController(BaseController): | ||||
|         try: | ||||
|             container, has_been_created = self._get_container() | ||||
|             if has_been_created: | ||||
|                 container.start() | ||||
|                 return None | ||||
|             # Check if the container is out of date, delete it and retry | ||||
|             if len(container.image.tags) > 0: | ||||
| @ -98,6 +124,11 @@ class DockerController(BaseController): | ||||
|                     ) | ||||
|                     self.down() | ||||
|                     return self.up() | ||||
|             # Check container's ports | ||||
|             if self._comp_ports(container): | ||||
|                 self.logger.info("Container has mis-matched ports, re-creating...") | ||||
|                 self.down() | ||||
|                 return self.up() | ||||
|             # Check that container values match our values | ||||
|             if self._comp_env(container): | ||||
|                 self.logger.info("Container has outdated config, re-creating...") | ||||
| @ -138,6 +169,7 @@ class DockerController(BaseController): | ||||
|                 self.logger.info("Container is not running, restarting...") | ||||
|                 container.start() | ||||
|                 return None | ||||
|             self.logger.info("Container is running") | ||||
|             return None | ||||
|         except DockerException as exc: | ||||
|             raise ControllerException(str(exc)) from exc | ||||
|  | ||||
| @ -405,7 +405,10 @@ class Outpost(models.Model): | ||||
|  | ||||
|     def get_required_objects(self) -> Iterable[Union[models.Model, str]]: | ||||
|         """Get an iterator of all objects the user needs read access to""" | ||||
|         objects: list[Union[models.Model, str]] = [self] | ||||
|         objects: list[Union[models.Model, str]] = [ | ||||
|             self, | ||||
|             "authentik_events.add_event", | ||||
|         ] | ||||
|         for provider in ( | ||||
|             Provider.objects.filter(outpost=self).select_related().select_subclasses() | ||||
|         ): | ||||
|  | ||||
| @ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = { | ||||
|     }, | ||||
|     "outposts_service_connection_check": { | ||||
|         "task": "authentik.outposts.tasks.outpost_service_connection_monitor", | ||||
|         "schedule": crontab(minute="*/60"), | ||||
|         "schedule": crontab(minute="*/5"), | ||||
|         "options": {"queue": "authentik_scheduled"}, | ||||
|     }, | ||||
|     "outpost_token_ensurer": { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| """authentik outpost signals""" | ||||
| from django.core.cache import cache | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import post_save, pre_delete, pre_save | ||||
| from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save | ||||
| from django.dispatch import receiver | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| @ -46,6 +46,14 @@ def pre_save_outpost(sender, instance: Outpost, **_): | ||||
|         outpost_controller.delay(instance.pk.hex, action="down", from_cache=True) | ||||
|  | ||||
|  | ||||
| @receiver(m2m_changed, sender=Outpost.providers.through) | ||||
| # pylint: disable=unused-argument | ||||
| def m2m_changed_update(sender, instance: Model, action: str, **_): | ||||
|     """Update outpost on m2m change, when providers are added or removed""" | ||||
|     if action in ["post_add", "post_remove", "post_clear"]: | ||||
|         outpost_post_save.delay(class_to_path(instance.__class__), instance.pk) | ||||
|  | ||||
|  | ||||
| @receiver(post_save) | ||||
| # pylint: disable=unused-argument | ||||
| def post_save_update(sender, instance: Model, **_): | ||||
|  | ||||
| @ -82,13 +82,13 @@ class PolicyBindingSerializer(ModelSerializer): | ||||
|             "timeout", | ||||
|         ] | ||||
|  | ||||
|     def validate(self, data: OrderedDict) -> OrderedDict: | ||||
|     def validate(self, attrs: OrderedDict) -> OrderedDict: | ||||
|         """Check that either policy, group or user is set.""" | ||||
|         count = sum( | ||||
|             [ | ||||
|                 bool(data.get("policy", None)), | ||||
|                 bool(data.get("group", None)), | ||||
|                 bool(data.get("user", None)), | ||||
|                 bool(attrs.get("policy", None)), | ||||
|                 bool(attrs.get("group", None)), | ||||
|                 bool(attrs.get("user", None)), | ||||
|             ] | ||||
|         ) | ||||
|         invalid = count > 1 | ||||
| @ -97,7 +97,7 @@ class PolicyBindingSerializer(ModelSerializer): | ||||
|             raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.") | ||||
|         if empty: | ||||
|             raise ValidationError("One of 'policy', 'group' or 'user' must be set.") | ||||
|         return data | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
| class PolicyBindingViewSet(UsedByMixin, ModelViewSet): | ||||
|  | ||||
| @ -62,12 +62,6 @@ class PolicyEngine: | ||||
|     # Allow objects with no policies attached to pass | ||||
|     empty_result: bool | ||||
|  | ||||
|     __pbm: PolicyBindingModel | ||||
|     __cached_policies: list[PolicyResult] | ||||
|     __processes: list[PolicyProcessInfo] | ||||
|  | ||||
|     __expected_result_count: int | ||||
|  | ||||
|     def __init__( | ||||
|         self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None | ||||
|     ): | ||||
| @ -83,8 +77,8 @@ class PolicyEngine: | ||||
|         self.request.obj = pbm | ||||
|         if request: | ||||
|             self.request.set_http_request(request) | ||||
|         self.__cached_policies = [] | ||||
|         self.__processes = [] | ||||
|         self.__cached_policies: list[PolicyResult] = [] | ||||
|         self.__processes: list[PolicyProcessInfo] = [] | ||||
|         self.use_cache = True | ||||
|         self.__expected_result_count = 0 | ||||
|  | ||||
|  | ||||
| @ -33,21 +33,21 @@ class ReputationPolicy(Policy): | ||||
|  | ||||
|     def passes(self, request: PolicyRequest) -> PolicyResult: | ||||
|         remote_ip = get_client_ip(request.http_request) | ||||
|         passing = True | ||||
|         passing = False | ||||
|         if self.check_ip: | ||||
|             score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0) | ||||
|             passing = passing and score <= self.threshold | ||||
|             passing += passing or score <= self.threshold | ||||
|             LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing) | ||||
|         if self.check_username: | ||||
|             score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0) | ||||
|             passing = passing and score <= self.threshold | ||||
|             passing += passing or score <= self.threshold | ||||
|             LOGGER.debug( | ||||
|                 "Score for Username", | ||||
|                 username=request.user.username, | ||||
|                 score=score, | ||||
|                 passing=passing, | ||||
|             ) | ||||
|         return PolicyResult(passing) | ||||
|         return PolicyResult(bool(passing)) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|  | ||||
| @ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer): | ||||
|             "expires", | ||||
|             "scope", | ||||
|             "id_token", | ||||
|             "revoked", | ||||
|         ] | ||||
|         depth = 2 | ||||
|  | ||||
|  | ||||
| @ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.4 on 2021-07-03 13:13 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_providers_oauth2", "0014_alter_oauth2provider_rsa_key"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="authorizationcode", | ||||
|             name="revoked", | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="refreshtoken", | ||||
|             name="revoked", | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @ -278,7 +278,7 @@ class OAuth2Provider(Provider): | ||||
|         """Guess launch_url based on first redirect_uri""" | ||||
|         if self.redirect_uris == "": | ||||
|             return None | ||||
|         main_url = self.redirect_uris.split("\n")[0] | ||||
|         main_url = self.redirect_uris.split("\n", maxsplit=1)[0] | ||||
|         launch_url = urlparse(main_url) | ||||
|         return main_url.replace(launch_url.path, "") | ||||
|  | ||||
| @ -318,6 +318,7 @@ class BaseGrantModel(models.Model): | ||||
|     provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE) | ||||
|     user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE) | ||||
|     _scope = models.TextField(default="", verbose_name=_("Scopes")) | ||||
|     revoked = models.BooleanField(default=False) | ||||
|  | ||||
|     @property | ||||
|     def scope(self) -> list[str]: | ||||
| @ -473,9 +474,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel): | ||||
|         # Convert datetimes into timestamps. | ||||
|         now = int(time.time()) | ||||
|         iat_time = now | ||||
|         exp_time = int( | ||||
|             now + timedelta_from_string(self.provider.token_validity).seconds | ||||
|         ) | ||||
|         exp_time = int(dateformat.format(self.expires, "U")) | ||||
|         # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time | ||||
|         auth_events = Event.objects.filter( | ||||
|             action=EventAction.LOGIN, user=get_user(user) | ||||
|  | ||||
| @ -6,6 +6,8 @@ from django.urls import reverse | ||||
| from django.utils.encoding import force_str | ||||
|  | ||||
| from authentik.core.models import Application, User | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.providers.oauth2.constants import ( | ||||
|     GRANT_TYPE_AUTHORIZATION_CODE, | ||||
| @ -39,7 +41,8 @@ class TestToken(OAuthTestCase): | ||||
|             client_id=generate_client_id(), | ||||
|             client_secret=generate_client_secret(), | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|             redirect_uris="http://local.invalid", | ||||
|             redirect_uris="http://testserver", | ||||
|             rsa_key=CertificateKeyPair.objects.first(), | ||||
|         ) | ||||
|         header = b64encode( | ||||
|             f"{provider.client_id}:{provider.client_secret}".encode() | ||||
| @ -53,11 +56,13 @@ class TestToken(OAuthTestCase): | ||||
|             data={ | ||||
|                 "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, | ||||
|                 "code": code.code, | ||||
|                 "redirect_uri": "http://local.invalid", | ||||
|                 "redirect_uri": "http://testserver", | ||||
|             }, | ||||
|             HTTP_AUTHORIZATION=f"Basic {header}", | ||||
|         ) | ||||
|         params = TokenParams.from_request(request) | ||||
|         params = TokenParams.parse( | ||||
|             request, provider, provider.client_id, provider.client_secret | ||||
|         ) | ||||
|         self.assertEqual(params.provider, provider) | ||||
|  | ||||
|     def test_request_refresh_token(self): | ||||
| @ -68,6 +73,7 @@ class TestToken(OAuthTestCase): | ||||
|             client_secret=generate_client_secret(), | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|             redirect_uris="http://local.invalid", | ||||
|             rsa_key=CertificateKeyPair.objects.first(), | ||||
|         ) | ||||
|         header = b64encode( | ||||
|             f"{provider.client_id}:{provider.client_secret}".encode() | ||||
| @ -87,7 +93,9 @@ class TestToken(OAuthTestCase): | ||||
|             }, | ||||
|             HTTP_AUTHORIZATION=f"Basic {header}", | ||||
|         ) | ||||
|         params = TokenParams.from_request(request) | ||||
|         params = TokenParams.parse( | ||||
|             request, provider, provider.client_id, provider.client_secret | ||||
|         ) | ||||
|         self.assertEqual(params.provider, provider) | ||||
|  | ||||
|     def test_auth_code_view(self): | ||||
| @ -98,6 +106,7 @@ class TestToken(OAuthTestCase): | ||||
|             client_secret=generate_client_secret(), | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|             redirect_uris="http://local.invalid", | ||||
|             rsa_key=CertificateKeyPair.objects.first(), | ||||
|         ) | ||||
|         # Needs to be assigned to an application for iss to be set | ||||
|         self.app.provider = provider | ||||
| @ -141,6 +150,7 @@ class TestToken(OAuthTestCase): | ||||
|             client_secret=generate_client_secret(), | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|             redirect_uris="http://local.invalid", | ||||
|             rsa_key=CertificateKeyPair.objects.first(), | ||||
|         ) | ||||
|         # Needs to be assigned to an application for iss to be set | ||||
|         self.app.provider = provider | ||||
| @ -193,6 +203,7 @@ class TestToken(OAuthTestCase): | ||||
|             client_secret=generate_client_secret(), | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|             redirect_uris="http://local.invalid", | ||||
|             rsa_key=CertificateKeyPair.objects.first(), | ||||
|         ) | ||||
|         header = b64encode( | ||||
|             f"{provider.client_id}:{provider.client_secret}".encode() | ||||
| @ -230,3 +241,65 @@ class TestToken(OAuthTestCase): | ||||
|                 ), | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_refresh_token_revoke(self): | ||||
|         """test request param""" | ||||
|         provider = OAuth2Provider.objects.create( | ||||
|             name="test", | ||||
|             client_id=generate_client_id(), | ||||
|             client_secret=generate_client_secret(), | ||||
|             authorization_flow=Flow.objects.first(), | ||||
|             redirect_uris="http://testserver", | ||||
|             rsa_key=CertificateKeyPair.objects.first(), | ||||
|         ) | ||||
|         # Needs to be assigned to an application for iss to be set | ||||
|         self.app.provider = provider | ||||
|         self.app.save() | ||||
|         header = b64encode( | ||||
|             f"{provider.client_id}:{provider.client_secret}".encode() | ||||
|         ).decode() | ||||
|         user = User.objects.get(username="akadmin") | ||||
|         token: RefreshToken = RefreshToken.objects.create( | ||||
|             provider=provider, | ||||
|             user=user, | ||||
|             refresh_token=generate_client_id(), | ||||
|         ) | ||||
|         # Create initial refresh token | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             data={ | ||||
|                 "grant_type": GRANT_TYPE_REFRESH_TOKEN, | ||||
|                 "refresh_token": token.refresh_token, | ||||
|                 "redirect_uri": "http://testserver", | ||||
|             }, | ||||
|             HTTP_AUTHORIZATION=f"Basic {header}", | ||||
|         ) | ||||
|         new_token: RefreshToken = ( | ||||
|             RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first() | ||||
|         ) | ||||
|         # Post again with initial token -> get new refresh token | ||||
|         # and revoke old one | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             data={ | ||||
|                 "grant_type": GRANT_TYPE_REFRESH_TOKEN, | ||||
|                 "refresh_token": new_token.refresh_token, | ||||
|                 "redirect_uri": "http://local.invalid", | ||||
|             }, | ||||
|             HTTP_AUTHORIZATION=f"Basic {header}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         # Post again with old token, is now revoked and should error | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_providers_oauth2:token"), | ||||
|             data={ | ||||
|                 "grant_type": GRANT_TYPE_REFRESH_TOKEN, | ||||
|                 "refresh_token": new_token.refresh_token, | ||||
|                 "redirect_uri": "http://local.invalid", | ||||
|             }, | ||||
|             HTTP_AUTHORIZATION=f"Basic {header}", | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists() | ||||
|         ) | ||||
|  | ||||
| @ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect | ||||
| from django.utils.cache import patch_vary_headers | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.providers.oauth2.errors import BearerTokenError | ||||
| from authentik.providers.oauth2.models import RefreshToken | ||||
|  | ||||
| @ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s | ||||
|     if not allowed: | ||||
|         LOGGER.warning( | ||||
|             "CORS: Origin is not an allowed origin", | ||||
|             requested=origin, | ||||
|             requested=received_origin, | ||||
|             allowed=allowed_origins, | ||||
|         ) | ||||
|         return response | ||||
| @ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]): | ||||
|                     raise BearerTokenError("invalid_token") | ||||
|  | ||||
|                 try: | ||||
|                     kwargs["token"] = RefreshToken.objects.get( | ||||
|                     token: RefreshToken = RefreshToken.objects.get( | ||||
|                         access_token=access_token | ||||
|                     ) | ||||
|                 except RefreshToken.DoesNotExist: | ||||
|                     LOGGER.debug("Token does not exist", access_token=access_token) | ||||
|                     raise BearerTokenError("invalid_token") | ||||
|  | ||||
|                 if kwargs["token"].is_expired: | ||||
|                 if token.is_expired: | ||||
|                     LOGGER.debug("Token has expired", access_token=access_token) | ||||
|                     raise BearerTokenError("invalid_token") | ||||
|  | ||||
|                 if not set(scopes).issubset(set(kwargs["token"].scope)): | ||||
|                 if token.revoked: | ||||
|                     LOGGER.warning("Revoked token was used", access_token=access_token) | ||||
|                     Event.new( | ||||
|                         action=EventAction.SUSPICIOUS_REQUEST, | ||||
|                         message="Revoked refresh token was used", | ||||
|                         token=access_token, | ||||
|                     ).from_http(request) | ||||
|                     raise BearerTokenError("invalid_token") | ||||
|  | ||||
|                 if not set(scopes).issubset(set(token.scope)): | ||||
|                     LOGGER.warning( | ||||
|                         "Scope missmatch.", | ||||
|                         required=set(scopes), | ||||
|                         token_has=set(kwargs["token"].scope), | ||||
|                         token_has=set(token.scope), | ||||
|                     ) | ||||
|                     raise BearerTokenError("insufficient_scope") | ||||
|             except BearerTokenError as error: | ||||
| @ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]): | ||||
|                     "WWW-Authenticate" | ||||
|                 ] = f'error="{error.code}", error_description="{error.description}"' | ||||
|                 return response | ||||
|  | ||||
|             kwargs["token"] = token | ||||
|             return view(request, *args, **kwargs) | ||||
|  | ||||
|         return view_wrapper | ||||
|  | ||||
| @ -374,9 +374,9 @@ class OAuthFulfillmentStage(StageView): | ||||
|             query_fragment["code"] = code.code | ||||
|  | ||||
|         query_fragment["token_type"] = "bearer" | ||||
|         query_fragment["expires_in"] = timedelta_from_string( | ||||
|             self.provider.token_validity | ||||
|         ).seconds | ||||
|         query_fragment["expires_in"] = int( | ||||
|             timedelta_from_string(self.provider.token_validity).total_seconds() | ||||
|         ) | ||||
|         query_fragment["state"] = self.params.state if self.params.state else "" | ||||
|  | ||||
|         return query_fragment | ||||
| @ -468,14 +468,14 @@ class AuthorizationFlowInitView(PolicyAccessView): | ||||
|         # OpenID clients can specify a `prompt` parameter, and if its set to consent we | ||||
|         # need to inject a consent stage | ||||
|         if PROMPT_CONSNET in self.params.prompt: | ||||
|             if not any(isinstance(x, ConsentStageView) for x in plan.stages): | ||||
|             if not any(isinstance(x.stage, ConsentStageView) for x in plan.bindings): | ||||
|                 # Plan does not have any consent stage, so we add an in-memory one | ||||
|                 stage = ConsentStage( | ||||
|                     name="OAuth2 Provider In-memory consent stage", | ||||
|                     mode=ConsentMode.ALWAYS_REQUIRE, | ||||
|                 ) | ||||
|                 plan.append(stage) | ||||
|         plan.append(in_memory_stage(OAuthFulfillmentStage)) | ||||
|                 plan.append_stage(stage) | ||||
|         plan.append_stage(in_memory_stage(OAuthFulfillmentStage)) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|  | ||||
| @ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse | ||||
| from django.views import View | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.providers.oauth2.constants import ( | ||||
|     GRANT_TYPE_AUTHORIZATION_CODE, | ||||
| @ -30,6 +31,7 @@ LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| # pylint: disable=too-many-instance-attributes | ||||
| class TokenParams: | ||||
|     """Token params""" | ||||
|  | ||||
| @ -40,6 +42,8 @@ class TokenParams: | ||||
|     state: str | ||||
|     scope: list[str] | ||||
|  | ||||
|     provider: OAuth2Provider | ||||
|  | ||||
|     authorization_code: Optional[AuthorizationCode] = None | ||||
|     refresh_token: Optional[RefreshToken] = None | ||||
|  | ||||
| @ -47,35 +51,34 @@ class TokenParams: | ||||
|  | ||||
|     raw_code: InitVar[str] = "" | ||||
|     raw_token: InitVar[str] = "" | ||||
|     request: InitVar[Optional[HttpRequest]] = None | ||||
|  | ||||
|     @staticmethod | ||||
|     def from_request(request: HttpRequest) -> "TokenParams": | ||||
|         """Extract Token Parameters from http request""" | ||||
|         client_id, client_secret = extract_client_auth(request) | ||||
|  | ||||
|     def parse( | ||||
|         request: HttpRequest, | ||||
|         provider: OAuth2Provider, | ||||
|         client_id: str, | ||||
|         client_secret: str, | ||||
|     ) -> "TokenParams": | ||||
|         """Parse params for request""" | ||||
|         return TokenParams( | ||||
|             # Init vars | ||||
|             raw_code=request.POST.get("code", ""), | ||||
|             raw_token=request.POST.get("refresh_token", ""), | ||||
|             request=request, | ||||
|             # Regular params | ||||
|             provider=provider, | ||||
|             client_id=client_id, | ||||
|             client_secret=client_secret, | ||||
|             redirect_uri=request.POST.get("redirect_uri", ""), | ||||
|             grant_type=request.POST.get("grant_type", ""), | ||||
|             raw_code=request.POST.get("code", ""), | ||||
|             raw_token=request.POST.get("refresh_token", ""), | ||||
|             state=request.POST.get("state", ""), | ||||
|             scope=request.POST.get("scope", "").split(), | ||||
|             # PKCE parameter. | ||||
|             code_verifier=request.POST.get("code_verifier"), | ||||
|         ) | ||||
|  | ||||
|     def __post_init__(self, raw_code, raw_token): | ||||
|         try: | ||||
|             provider: OAuth2Provider = OAuth2Provider.objects.get( | ||||
|                 client_id=self.client_id | ||||
|             ) | ||||
|             self.provider = provider | ||||
|         except OAuth2Provider.DoesNotExist: | ||||
|             LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id) | ||||
|             raise TokenError("invalid_client") | ||||
|  | ||||
|     def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest): | ||||
|         if self.provider.client_type == ClientTypes.CONFIDENTIAL: | ||||
|             if self.provider.client_secret != self.client_secret: | ||||
|                 LOGGER.warning( | ||||
| @ -87,7 +90,6 @@ class TokenParams: | ||||
|  | ||||
|         if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: | ||||
|             self.__post_init_code(raw_code) | ||||
|  | ||||
|         elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN: | ||||
|             if not raw_token: | ||||
|                 LOGGER.warning("Missing refresh token") | ||||
| @ -107,7 +109,14 @@ class TokenParams: | ||||
|                     token=raw_token, | ||||
|                 ) | ||||
|                 raise TokenError("invalid_grant") | ||||
|  | ||||
|             if self.refresh_token.revoked: | ||||
|                 LOGGER.warning("Refresh token is revoked", token=raw_token) | ||||
|                 Event.new( | ||||
|                     action=EventAction.SUSPICIOUS_REQUEST, | ||||
|                     message="Revoked refresh token was used", | ||||
|                     token=raw_token, | ||||
|                 ).from_http(request) | ||||
|                 raise TokenError("invalid_grant") | ||||
|         else: | ||||
|             LOGGER.warning("Invalid grant type", grant_type=self.grant_type) | ||||
|             raise TokenError("unsupported_grant_type") | ||||
| @ -159,13 +168,14 @@ class TokenParams: | ||||
| class TokenView(View): | ||||
|     """Generate tokens for clients""" | ||||
|  | ||||
|     provider: Optional[OAuth2Provider] = None | ||||
|     params: Optional[TokenParams] = None | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|         response = super().dispatch(request, *args, **kwargs) | ||||
|         allowed_origins = [] | ||||
|         if self.params: | ||||
|             allowed_origins = self.params.provider.redirect_uris.split("\n") | ||||
|         if self.provider: | ||||
|             allowed_origins = self.provider.redirect_uris.split("\n") | ||||
|         cors_allow(self.request, response, *allowed_origins) | ||||
|         return response | ||||
|  | ||||
| @ -175,19 +185,32 @@ class TokenView(View): | ||||
|     def post(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Generate tokens for clients""" | ||||
|         try: | ||||
|             self.params = TokenParams.from_request(request) | ||||
|             client_id, client_secret = extract_client_auth(request) | ||||
|             try: | ||||
|                 self.provider = OAuth2Provider.objects.get(client_id=client_id) | ||||
|             except OAuth2Provider.DoesNotExist: | ||||
|                 LOGGER.warning( | ||||
|                     "OAuth2Provider does not exist", client_id=self.client_id | ||||
|                 ) | ||||
|                 raise TokenError("invalid_client") | ||||
|  | ||||
|             if not self.provider: | ||||
|                 raise ValueError | ||||
|             self.params = TokenParams.parse( | ||||
|                 request, self.provider, client_id, client_secret | ||||
|             ) | ||||
|  | ||||
|             if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE: | ||||
|                 return TokenResponse(self.create_code_response_dic()) | ||||
|                 return TokenResponse(self.create_code_response()) | ||||
|             if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN: | ||||
|                 return TokenResponse(self.create_refresh_response_dic()) | ||||
|                 return TokenResponse(self.create_refresh_response()) | ||||
|             raise ValueError(f"Invalid grant_type: {self.params.grant_type}") | ||||
|         except TokenError as error: | ||||
|             return TokenResponse(error.create_dict(), status=400) | ||||
|         except UserAuthError as error: | ||||
|             return TokenResponse(error.create_dict(), status=403) | ||||
|  | ||||
|     def create_code_response_dic(self) -> dict[str, Any]: | ||||
|     def create_code_response(self) -> dict[str, Any]: | ||||
|         """See https://tools.ietf.org/html/rfc6749#section-4.1""" | ||||
|  | ||||
|         refresh_token = self.params.authorization_code.provider.create_refresh_token( | ||||
| @ -211,19 +234,19 @@ class TokenView(View): | ||||
|         # We don't need to store the code anymore. | ||||
|         self.params.authorization_code.delete() | ||||
|  | ||||
|         response_dict = { | ||||
|         return { | ||||
|             "access_token": refresh_token.access_token, | ||||
|             "refresh_token": refresh_token.refresh_token, | ||||
|             "token_type": "bearer", | ||||
|             "expires_in": timedelta_from_string( | ||||
|                 self.params.provider.token_validity | ||||
|             ).seconds, | ||||
|             "expires_in": int( | ||||
|                 timedelta_from_string( | ||||
|                     self.params.provider.token_validity | ||||
|                 ).total_seconds() | ||||
|             ), | ||||
|             "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()), | ||||
|         } | ||||
|  | ||||
|         return response_dict | ||||
|  | ||||
|     def create_refresh_response_dic(self) -> dict[str, Any]: | ||||
|     def create_refresh_response(self) -> dict[str, Any]: | ||||
|         """See https://tools.ietf.org/html/rfc6749#section-6""" | ||||
|  | ||||
|         unauthorized_scopes = set(self.params.scope) - set( | ||||
| @ -251,17 +274,18 @@ class TokenView(View): | ||||
|             # Store the refresh_token. | ||||
|             refresh_token.save() | ||||
|  | ||||
|         # Forget the old token. | ||||
|         self.params.refresh_token.delete() | ||||
|         # Mark old token as revoked | ||||
|         self.params.refresh_token.revoked = True | ||||
|         self.params.refresh_token.save() | ||||
|  | ||||
|         dic = { | ||||
|         return { | ||||
|             "access_token": refresh_token.access_token, | ||||
|             "refresh_token": refresh_token.refresh_token, | ||||
|             "token_type": "bearer", | ||||
|             "expires_in": timedelta_from_string( | ||||
|                 refresh_token.provider.token_validity | ||||
|             ).seconds, | ||||
|             "expires_in": int( | ||||
|                 timedelta_from_string( | ||||
|                     refresh_token.provider.token_validity | ||||
|                 ).total_seconds() | ||||
|             ), | ||||
|             "id_token": self.params.provider.encode(refresh_token.id_token.to_dict()), | ||||
|         } | ||||
|  | ||||
|         return dic | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """authentik OAuth2 OpenID Userinfo views""" | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from deepmerge import always_merger | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.http.response import HttpResponseBadRequest | ||||
| from django.views import View | ||||
| @ -78,7 +79,7 @@ class UserInfoView(View): | ||||
|                 ) | ||||
|                 continue | ||||
|             LOGGER.debug("updated scope", scope=scope) | ||||
|             final_claims.update(value) | ||||
|             always_merger.merge(final_claims, value) | ||||
|         return final_claims | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|  | ||||
| @ -79,7 +79,7 @@ class SAMLSSOView(PolicyAccessView): | ||||
|                 PLAN_CONTEXT_CONSENT_PERMISSIONS: [], | ||||
|             }, | ||||
|         ) | ||||
|         plan.append(in_memory_stage(SAMLFlowFinalView)) | ||||
|         plan.append_stage(in_memory_stage(SAMLFlowFinalView)) | ||||
|         request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|  | ||||
| @ -15,7 +15,7 @@ class MessageConsumer(JsonWebsocketConsumer): | ||||
|         cache.set(f"user_{self.session_key}_messages_{self.channel_name}", True, None) | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def disconnect(self, close_code): | ||||
|     def disconnect(self, code): | ||||
|         cache.delete(f"user_{self.session_key}_messages_{self.channel_name}") | ||||
|  | ||||
|     def event_update(self, event: dict): | ||||
|  | ||||
| @ -153,6 +153,7 @@ SPECTACULAR_SETTINGS = { | ||||
|         "url": "https://github.com/goauthentik/authentik/blob/master/LICENSE", | ||||
|     }, | ||||
|     "ENUM_NAME_OVERRIDES": { | ||||
|         "EventActions": "authentik.events.models.EventAction", | ||||
|         "ChallengeChoices": "authentik.flows.challenge.ChallengeTypes", | ||||
|         "FlowDesignationEnum": "authentik.flows.models.FlowDesignation", | ||||
|         "PolicyEngineMode": "authentik.policies.models.PolicyEngineMode", | ||||
|  | ||||
| @ -60,14 +60,21 @@ class LDAPPasswordChanger: | ||||
|     def check_ad_password_complexity_enabled(self) -> bool: | ||||
|         """Check if DOMAIN_PASSWORD_COMPLEX is enabled""" | ||||
|         root_dn = self.get_domain_root_dn() | ||||
|         root_attrs = self._source.connection.extend.standard.paged_search( | ||||
|             search_base=root_dn, | ||||
|             search_filter="(objectClass=*)", | ||||
|             search_scope=ldap3.BASE, | ||||
|             attributes=["pwdProperties"], | ||||
|         ) | ||||
|         try: | ||||
|             root_attrs = self._source.connection.extend.standard.paged_search( | ||||
|                 search_base=root_dn, | ||||
|                 search_filter="(objectClass=*)", | ||||
|                 search_scope=ldap3.BASE, | ||||
|                 attributes=["pwdProperties"], | ||||
|             ) | ||||
|         except ldap3.core.exceptions.LDAPAttributeError: | ||||
|             return False | ||||
|         root_attrs = list(root_attrs)[0] | ||||
|         pwd_properties = PwdProperties(root_attrs["attributes"]["pwdProperties"]) | ||||
|         raw_pwd_properties = root_attrs.get("attributes", {}).get("pwdProperties", None) | ||||
|         if raw_pwd_properties is None: | ||||
|             return False | ||||
|  | ||||
|         pwd_properties = PwdProperties(raw_pwd_properties) | ||||
|         if PwdProperties.DOMAIN_PASSWORD_COMPLEX in pwd_properties: | ||||
|             return True | ||||
|  | ||||
|  | ||||
| @ -36,7 +36,8 @@ class SourceType: | ||||
| class SourceTypeManager: | ||||
|     """Manager to hold all Source types.""" | ||||
|  | ||||
|     __sources: list[SourceType] = [] | ||||
|     def __init__(self) -> None: | ||||
|         self.__sources: list[SourceType] = [] | ||||
|  | ||||
|     def type(self): | ||||
|         """Class decorator to register classes inline.""" | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """OAuth Callback Views""" | ||||
| from json import JSONDecodeError | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.conf import settings | ||||
| @ -10,6 +11,7 @@ from django.views.generic import View | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.sources.flow_manager import SourceFlowManager | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | ||||
| from authentik.sources.oauth.views.base import OAuthClientMixin | ||||
|  | ||||
| @ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View): | ||||
|         if "error" in token: | ||||
|             return self.handle_login_failure(token["error"]) | ||||
|         # Fetch profile info | ||||
|         raw_info = client.get_profile_info(token) | ||||
|         if raw_info is None: | ||||
|         try: | ||||
|             raw_info = client.get_profile_info(token) | ||||
|             if raw_info is None: | ||||
|                 return self.handle_login_failure("Could not retrieve profile.") | ||||
|         except JSONDecodeError as exc: | ||||
|             Event.new( | ||||
|                 EventAction.CONFIGURATION_ERROR, | ||||
|                 message="Failed to JSON-decode profile.", | ||||
|                 raw_profile=exc.doc, | ||||
|             ).from_http(self.request) | ||||
|             return self.handle_login_failure("Could not retrieve profile.") | ||||
|         identifier = self.get_user_id(raw_info) | ||||
|         if identifier is None: | ||||
|  | ||||
| @ -90,7 +90,7 @@ class InitiateView(View): | ||||
|         planner.allow_empty_flows = True | ||||
|         plan = planner.plan(self.request, kwargs) | ||||
|         for stage in stages_to_append: | ||||
|             plan.append(stage) | ||||
|             plan.append_stage(stage) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|  | ||||
| @ -63,7 +63,7 @@ class AuthenticatorDuoStageView(ChallengeStageView): | ||||
|                 "type": ChallengeTypes.NATIVE.value, | ||||
|                 "activation_barcode": enroll["activation_barcode"], | ||||
|                 "activation_code": enroll["activation_code"], | ||||
|                 "stage_uuid": stage.stage_uuid, | ||||
|                 "stage_uuid": str(stage.stage_uuid), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -74,12 +74,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||
|             duo, self.stage.request, self.stage.get_pending_user() | ||||
|         ) | ||||
|  | ||||
|     def validate(self, data: dict): | ||||
|     def validate(self, attrs: dict): | ||||
|         # Checking if the given data is from a valid device class is done above | ||||
|         # Here we only check if the any data was sent at all | ||||
|         if "code" not in data and "webauthn" not in data and "duo" not in data: | ||||
|         if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs: | ||||
|             raise ValidationError("Empty response") | ||||
|         return data | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
| class AuthenticatorValidateStageView(ChallengeStageView): | ||||
| @ -148,7 +148,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|                 stage = Stage.objects.get_subclass(pk=stage.configuration_stage.pk) | ||||
|                 # plan.insert inserts at 1 index, so when stage_ok pops 0, | ||||
|                 # the configuration stage is next | ||||
|                 self.executor.plan.insert(stage) | ||||
|                 self.executor.plan.insert_stage(stage) | ||||
|                 return self.executor.stage_ok() | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
| @ -163,7 +163,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def challenge_valid( | ||||
|         self, challenge: AuthenticatorValidationChallengeResponse | ||||
|         self, response: AuthenticatorValidationChallengeResponse | ||||
|     ) -> HttpResponse: | ||||
|         # All validation is done by the serializer | ||||
|         return self.executor.stage_ok() | ||||
|  | ||||
| @ -36,12 +36,14 @@ class TestCaptchaStage(TestCase): | ||||
|             public_key=RECAPTCHA_PUBLIC_KEY, | ||||
|             private_key=RECAPTCHA_PRIVATE_KEY, | ||||
|         ) | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_valid(self): | ||||
|         """Test valid captcha""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|  | ||||
| @ -39,9 +39,11 @@ class TestConsentStage(TestCase): | ||||
|         stage = ConsentStage.objects.create( | ||||
|             name="consent", mode=ConsentMode.ALWAYS_REQUIRE | ||||
|         ) | ||||
|         FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|         binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|  | ||||
|         plan = FlowPlan(flow_pk=flow.pk.hex, stages=[stage], markers=[StageMarker()]) | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
| @ -69,11 +71,11 @@ class TestConsentStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.PERMANENT) | ||||
|         FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|         binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|  | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=flow.pk.hex, | ||||
|             stages=[stage], | ||||
|             bindings=[binding], | ||||
|             markers=[StageMarker()], | ||||
|             context={PLAN_CONTEXT_APPLICATION: self.application}, | ||||
|         ) | ||||
| @ -110,11 +112,11 @@ class TestConsentStage(TestCase): | ||||
|         stage = ConsentStage.objects.create( | ||||
|             name="consent", mode=ConsentMode.EXPIRING, consent_expire_in="seconds=1" | ||||
|         ) | ||||
|         FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|         binding = FlowStageBinding.objects.create(target=flow, stage=stage, order=2) | ||||
|  | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=flow.pk.hex, | ||||
|             stages=[stage], | ||||
|             bindings=[binding], | ||||
|             markers=[StageMarker()], | ||||
|             context={PLAN_CONTEXT_APPLICATION: self.application}, | ||||
|         ) | ||||
|  | ||||
| @ -26,12 +26,14 @@ class TestUserDenyStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         self.stage = DenyStage.objects.create(name="logout") | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_valid_password(self): | ||||
|         """Test with a valid pending user and backend""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|  | ||||
| @ -38,7 +38,7 @@ class EmailChallengeResponse(ChallengeResponse): | ||||
|  | ||||
|     component = CharField(default="ak-stage-email") | ||||
|  | ||||
|     def validate(self, data): | ||||
|     def validate(self, attrs): | ||||
|         raise ValidationError("") | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -34,12 +34,14 @@ class TestEmailStageSending(TestCase): | ||||
|         self.stage = EmailStage.objects.create( | ||||
|             name="email", | ||||
|         ) | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_pending_user(self): | ||||
|         """Test with pending user""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -67,7 +69,7 @@ class TestEmailStageSending(TestCase): | ||||
|     def test_send_error(self): | ||||
|         """Test error during sending (sending will be retried)""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|  | ||||
| @ -35,12 +35,14 @@ class TestEmailStage(TestCase): | ||||
|         self.stage = EmailStage.objects.create( | ||||
|             name="email", | ||||
|         ) | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_rendering(self): | ||||
|         """Test with pending user""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -56,7 +58,7 @@ class TestEmailStage(TestCase): | ||||
|     def test_without_user(self): | ||||
|         """Test without pending user""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -71,7 +73,7 @@ class TestEmailStage(TestCase): | ||||
|     def test_pending_user(self): | ||||
|         """Test with pending user""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -102,7 +104,7 @@ class TestEmailStage(TestCase): | ||||
|         # Make sure token exists | ||||
|         self.test_pending_user() | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|  | ||||
| @ -73,9 +73,9 @@ class IdentificationChallengeResponse(ChallengeResponse): | ||||
|  | ||||
|     pre_user: Optional[User] = None | ||||
|  | ||||
|     def validate(self, data: dict[str, Any]) -> dict[str, Any]: | ||||
|     def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: | ||||
|         """Validate that user exists, and optionally their password""" | ||||
|         uid_field = data["uid_field"] | ||||
|         uid_field = attrs["uid_field"] | ||||
|         current_stage: IdentificationStage = self.stage.executor.current_stage | ||||
|  | ||||
|         pre_user = self.stage.get_user(uid_field) | ||||
| @ -85,13 +85,25 @@ class IdentificationChallengeResponse(ChallengeResponse): | ||||
|             identification_failed.send( | ||||
|                 sender=self, request=self.stage.request, uid_field=uid_field | ||||
|             ) | ||||
|             # We set the pending_user even on failure so it's part of the context, even | ||||
|             # when the input is invalid | ||||
|             # This is so its part of the current flow plan, and on flow restart can be kept, and | ||||
|             # policies can be applied. | ||||
|             self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User( | ||||
|                 username=uid_field, | ||||
|                 email=uid_field, | ||||
|             ) | ||||
|             if not current_stage.show_matched_user: | ||||
|                 self.stage.executor.plan.context[ | ||||
|                     PLAN_CONTEXT_PENDING_USER_IDENTIFIER | ||||
|                 ] = uid_field | ||||
|             raise ValidationError("Failed to authenticate.") | ||||
|         self.pre_user = pre_user | ||||
|         if not current_stage.password_stage: | ||||
|             # No password stage select, don't validate the password | ||||
|             return data | ||||
|             return attrs | ||||
|  | ||||
|         password = data["password"] | ||||
|         password = attrs["password"] | ||||
|         try: | ||||
|             user = authenticate( | ||||
|                 self.stage.request, | ||||
| @ -104,7 +116,7 @@ class IdentificationChallengeResponse(ChallengeResponse): | ||||
|             self.pre_user = user | ||||
|         except PermissionDenied as exc: | ||||
|             raise ValidationError(str(exc)) from exc | ||||
|         return data | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
| class IdentificationStageView(ChallengeStageView): | ||||
|  | ||||
| @ -35,7 +35,9 @@ class TestUserLoginStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         self.stage = InvitationStage.objects.create(name="invitation") | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.to_stage_response", | ||||
| @ -44,7 +46,7 @@ class TestUserLoginStage(TestCase): | ||||
|     def test_without_invitation_fail(self): | ||||
|         """Test without any invitation, continue_flow_without_invitation not set.""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO | ||||
| @ -75,7 +77,7 @@ class TestUserLoginStage(TestCase): | ||||
|         self.stage.continue_flow_without_invitation = True | ||||
|         self.stage.save() | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO | ||||
| @ -103,7 +105,7 @@ class TestUserLoginStage(TestCase): | ||||
|     def test_with_invitation_get(self): | ||||
|         """Test with invitation, check data in session""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -143,7 +145,7 @@ class TestUserLoginStage(TestCase): | ||||
|         ) | ||||
|  | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex} | ||||
|         session = self.client.session | ||||
|  | ||||
| @ -39,7 +39,9 @@ class TestPasswordStage(TestCase): | ||||
|         self.stage = PasswordStage.objects.create( | ||||
|             name="password", backends=[BACKEND_DJANGO] | ||||
|         ) | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.to_stage_response", | ||||
| @ -48,7 +50,7 @@ class TestPasswordStage(TestCase): | ||||
|     def test_without_user(self): | ||||
|         """Test without user""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -84,7 +86,7 @@ class TestPasswordStage(TestCase): | ||||
|         ) | ||||
|  | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -101,7 +103,7 @@ class TestPasswordStage(TestCase): | ||||
|     def test_valid_password(self): | ||||
|         """Test with a valid pending user and valid password""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -129,7 +131,7 @@ class TestPasswordStage(TestCase): | ||||
|     def test_invalid_password(self): | ||||
|         """Test with a valid pending user and invalid password""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -148,7 +150,7 @@ class TestPasswordStage(TestCase): | ||||
|     def test_invalid_password_lockout(self): | ||||
|         """Test with a valid pending user and invalid password (trigger logout counter)""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -189,7 +191,7 @@ class TestPasswordStage(TestCase): | ||||
|         """Test with a valid pending user and valid password. | ||||
|         Backend is patched to return PermissionError""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|  | ||||
| @ -90,6 +90,14 @@ class PromptChallengeResponse(ChallengeResponse): | ||||
|             raise ValidationError(_("Passwords don't match.")) | ||||
|  | ||||
|     def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: | ||||
|         # Check if we have any static or hidden fields, and ensure they | ||||
|         # still have the same value | ||||
|         static_hidden_fields: QuerySet[Prompt] = self.stage.fields.filter( | ||||
|             type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC] | ||||
|         ) | ||||
|         for static_hidden in static_hidden_fields: | ||||
|             attrs[static_hidden.field_key] = static_hidden.placeholder | ||||
|  | ||||
|         # Check if we have two password fields, and make sure they are the same | ||||
|         password_fields: QuerySet[Prompt] = self.stage.fields.filter( | ||||
|             type=FieldTypes.PASSWORD | ||||
| @ -138,8 +146,6 @@ def password_single_validator_factory() -> Callable[[PromptChallenge, str], Any] | ||||
| class ListPolicyEngine(PolicyEngine): | ||||
|     """Slightly modified policy engine, which uses a list instead of a PolicyBindingModel""" | ||||
|  | ||||
|     __list: list[Policy] | ||||
|  | ||||
|     def __init__( | ||||
|         self, policies: list[Policy], user: User, request: HttpRequest = None | ||||
|     ) -> None: | ||||
|  | ||||
| @ -78,6 +78,12 @@ class TestPromptStage(TestCase): | ||||
|             required=True, | ||||
|             placeholder="HIDDEN_PLACEHOLDER", | ||||
|         ) | ||||
|         static_prompt = Prompt.objects.create( | ||||
|             field_key="static_prompt", | ||||
|             type=FieldTypes.STATIC, | ||||
|             required=True, | ||||
|             placeholder="static", | ||||
|         ) | ||||
|         self.stage = PromptStage.objects.create(name="prompt-stage") | ||||
|         self.stage.fields.set( | ||||
|             [ | ||||
| @ -88,6 +94,7 @@ class TestPromptStage(TestCase): | ||||
|                 password2_prompt, | ||||
|                 number_prompt, | ||||
|                 hidden_prompt, | ||||
|                 static_prompt, | ||||
|             ] | ||||
|         ) | ||||
|         self.stage.save() | ||||
| @ -100,14 +107,17 @@ class TestPromptStage(TestCase): | ||||
|             password2_prompt.field_key: "test", | ||||
|             number_prompt.field_key: 3, | ||||
|             hidden_prompt.field_key: hidden_prompt.placeholder, | ||||
|             static_prompt.field_key: static_prompt.placeholder, | ||||
|         } | ||||
|  | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_render(self): | ||||
|         """Test render of form, check if all prompts are rendered correctly""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -125,7 +135,7 @@ class TestPromptStage(TestCase): | ||||
|     def test_valid_challenge_with_policy(self) -> PromptChallengeResponse: | ||||
|         """Test challenge_response validation""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         expr = "return request.context['password_prompt'] == request.context['password2_prompt']" | ||||
|         expr_policy = ExpressionPolicy.objects.create( | ||||
| @ -142,7 +152,7 @@ class TestPromptStage(TestCase): | ||||
|     def test_invalid_challenge(self) -> PromptChallengeResponse: | ||||
|         """Test challenge_response validation""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         expr = "False" | ||||
|         expr_policy = ExpressionPolicy.objects.create( | ||||
| @ -159,7 +169,7 @@ class TestPromptStage(TestCase): | ||||
|     def test_valid_challenge_request(self): | ||||
|         """Test a request with valid challenge_response data""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -196,7 +206,7 @@ class TestPromptStage(TestCase): | ||||
|     def test_invalid_password(self): | ||||
|         """Test challenge_response validation""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         self.prompt_data["password2_prompt"] = "qwerqwerqr" | ||||
|         challenge_response = PromptChallengeResponse( | ||||
| @ -215,7 +225,7 @@ class TestPromptStage(TestCase): | ||||
|     def test_invalid_username(self): | ||||
|         """Test challenge_response validation""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         self.prompt_data["username_prompt"] = "akadmin" | ||||
|         challenge_response = PromptChallengeResponse( | ||||
| @ -230,3 +240,17 @@ class TestPromptStage(TestCase): | ||||
|                 ] | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_static_hidden_overwrite(self): | ||||
|         """Test that static and hidden fields ignore any value sent to them""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         self.prompt_data["hidden_prompt"] = "foo" | ||||
|         self.prompt_data["static_prompt"] = "foo" | ||||
|         challenge_response = PromptChallengeResponse( | ||||
|             None, stage=self.stage, plan=plan, data=self.prompt_data | ||||
|         ) | ||||
|         self.assertEqual(challenge_response.is_valid(), True) | ||||
|         self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo") | ||||
|         self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo") | ||||
|  | ||||
| @ -30,7 +30,9 @@ class TestUserDeleteStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         self.stage = UserDeleteStage.objects.create(name="delete") | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.flows.views.to_stage_response", | ||||
| @ -39,7 +41,7 @@ class TestUserDeleteStage(TestCase): | ||||
|     def test_no_user(self): | ||||
|         """Test without user set""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -66,7 +68,7 @@ class TestUserDeleteStage(TestCase): | ||||
|     def test_user_delete_get(self): | ||||
|         """Test Form render""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
|  | ||||
| @ -30,12 +30,14 @@ class TestUserLoginStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         self.stage = UserLoginStage.objects.create(name="login") | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_valid_password(self): | ||||
|         """Test with a valid pending user and backend""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -61,7 +63,7 @@ class TestUserLoginStage(TestCase): | ||||
|         self.stage.session_duration = "seconds=2" | ||||
|         self.stage.save() | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         session = self.client.session | ||||
| @ -92,7 +94,7 @@ class TestUserLoginStage(TestCase): | ||||
|     def test_without_user(self): | ||||
|         """Test a plan without any pending user, resulting in a denied""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|  | ||||
| @ -28,12 +28,14 @@ class TestUserLogoutStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         self.stage = UserLogoutStage.objects.create(name="logout") | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|  | ||||
|     def test_valid_password(self): | ||||
|         """Test with a valid pending user and backend""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = self.user | ||||
|         plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO | ||||
|  | ||||
| @ -12,7 +12,7 @@ class UserWriteStageSerializer(StageSerializer): | ||||
|     class Meta: | ||||
|  | ||||
|         model = UserWriteStage | ||||
|         fields = StageSerializer.Meta.fields | ||||
|         fields = StageSerializer.Meta.fields + ["create_users_as_inactive"] | ||||
|  | ||||
|  | ||||
| class UserWriteStageViewSet(UsedByMixin, ModelViewSet): | ||||
|  | ||||
| @ -0,0 +1,21 @@ | ||||
| # Generated by Django 3.2.4 on 2021-06-28 20:31 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_stages_user_write", "0002_auto_20200918_1653"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="userwritestage", | ||||
|             name="create_users_as_inactive", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 help_text="When set, newly created users are inactive and cannot login.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -1,6 +1,7 @@ | ||||
| """write stage models""" | ||||
| from typing import Type | ||||
|  | ||||
| from django.db import models | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views import View | ||||
| from rest_framework.serializers import BaseSerializer | ||||
| @ -12,6 +13,11 @@ class UserWriteStage(Stage): | ||||
|     """Writes currently pending data into the pending user, or if no user exists, | ||||
|     creates a new user with the data.""" | ||||
|  | ||||
|     create_users_as_inactive = models.BooleanField( | ||||
|         default=False, | ||||
|         help_text=_("When set, newly created users are inactive and cannot login."), | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> BaseSerializer: | ||||
|         from authentik.stages.user_write.api import UserWriteStageSerializer | ||||
|  | ||||
| @ -24,6 +24,10 @@ LOGGER = get_logger() | ||||
| class UserWriteStageView(StageView): | ||||
|     """Finalise Enrollment flow by creating a user object.""" | ||||
|  | ||||
|     def post(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Wrapper for post requests""" | ||||
|         return self.get(request) | ||||
|  | ||||
|     def get(self, request: HttpRequest) -> HttpResponse: | ||||
|         """Save data in the current flow to the currently pending user. If no user is pending, | ||||
|         a new user is created.""" | ||||
| @ -35,7 +39,9 @@ class UserWriteStageView(StageView): | ||||
|         data = self.executor.plan.context[PLAN_CONTEXT_PROMPT] | ||||
|         user_created = False | ||||
|         if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: | ||||
|             self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User() | ||||
|             self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User( | ||||
|                 is_active=not self.executor.current_stage.create_users_as_inactive | ||||
|             ) | ||||
|             self.executor.plan.context[ | ||||
|                 PLAN_CONTEXT_AUTHENTICATION_BACKEND | ||||
|             ] = class_to_path(ModelBackend) | ||||
|  | ||||
| @ -37,7 +37,9 @@ class TestUserWriteStage(TestCase): | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         self.stage = UserWriteStage.objects.create(name="write") | ||||
|         FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) | ||||
|         self.binding = FlowStageBinding.objects.create( | ||||
|             target=self.flow, stage=self.stage, order=2 | ||||
|         ) | ||||
|         self.source = Source.objects.create(name="fake_source") | ||||
|  | ||||
|     def test_user_create(self): | ||||
| @ -48,7 +50,7 @@ class TestUserWriteStage(TestCase): | ||||
|         ) | ||||
|  | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PROMPT] = { | ||||
|             "username": "test-user", | ||||
| @ -92,7 +94,7 @@ class TestUserWriteStage(TestCase): | ||||
|             for _ in range(8) | ||||
|         ) | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create( | ||||
|             username="unittest", email="test@beryju.org" | ||||
| @ -135,7 +137,7 @@ class TestUserWriteStage(TestCase): | ||||
|     def test_without_data(self): | ||||
|         """Test without data results in error""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
| @ -167,7 +169,7 @@ class TestUserWriteStage(TestCase): | ||||
|     def test_blank_username(self): | ||||
|         """Test with blank username results in error""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         plan.context[PLAN_CONTEXT_PROMPT] = { | ||||
| @ -204,7 +206,7 @@ class TestUserWriteStage(TestCase): | ||||
|     def test_duplicate_data(self): | ||||
|         """Test with duplicate data, should trigger error""" | ||||
|         plan = FlowPlan( | ||||
|             flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] | ||||
|             flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()] | ||||
|         ) | ||||
|         session = self.client.session | ||||
|         plan.context[PLAN_CONTEXT_PROMPT] = { | ||||
|  | ||||
| @ -54,6 +54,9 @@ class CurrentTenantSerializer(PassiveSerializer): | ||||
|         default=CONFIG.y("footer_links", []), | ||||
|     ) | ||||
|  | ||||
|     flow_authentication = CharField(source="flow_authentication.slug", required=False) | ||||
|     flow_invalidation = CharField(source="flow_invalidation.slug", required=False) | ||||
|     flow_recovery = CharField(source="flow_recovery.slug", required=False) | ||||
|     flow_unenrollment = CharField(source="flow_unenrollment.slug", required=False) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -20,6 +20,8 @@ class TestTenants(TestCase): | ||||
|                 "branding_title": "authentik", | ||||
|                 "matched_domain": "authentik-default", | ||||
|                 "ui_footer_links": CONFIG.y("footer_links"), | ||||
|                 "flow_authentication": "default-authentication-flow", | ||||
|                 "flow_invalidation": "default-invalidation-flow", | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -21,7 +21,7 @@ services: | ||||
|     networks: | ||||
|       - internal | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.2} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -44,7 +44,7 @@ services: | ||||
|       - "0.0.0.0:9000:9000" | ||||
|       - "0.0.0.0:9443:9443" | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.2} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.4} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     networks: | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| package constants | ||||
|  | ||||
| const VERSION = "2021.6.2" | ||||
| const VERSION = "2021.6.4" | ||||
|  | ||||
| @ -10,15 +10,19 @@ import ( | ||||
|  | ||||
| func (ws *WebServer) configureStatic() { | ||||
| 	statRouter := ws.lh.NewRoute().Subrouter() | ||||
| 	// Media files, always local | ||||
| 	fs := http.FileServer(http.Dir(config.G.Paths.Media)) | ||||
| 	if config.G.Debug || config.G.Web.LoadLocalFiles { | ||||
| 		ws.log.Debug("Using local static files") | ||||
| 		ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist")))) | ||||
| 		ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik")))) | ||||
| 		statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static/dist", http.FileServer(http.Dir("./web/dist")))) | ||||
| 		statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static/authentik", http.FileServer(http.Dir("./web/authentik")))) | ||||
| 		statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs)) | ||||
| 	} else { | ||||
| 		statRouter.Use(ws.staticHeaderMiddleware) | ||||
| 		ws.log.Debug("Using packaged static files with aggressive caching") | ||||
| 		ws.lh.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist)))) | ||||
| 		ws.lh.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik)))) | ||||
| 		statRouter.PathPrefix("/static/dist").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticDist)))) | ||||
| 		statRouter.PathPrefix("/static/authentik").Handler(http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik)))) | ||||
| 		statRouter.PathPrefix("/media").Handler(http.StripPrefix("/media", fs)) | ||||
| 	} | ||||
| 	ws.lh.Path("/robots.txt").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		rw.Header()["Content-Type"] = []string{"text/plain"} | ||||
| @ -30,8 +34,6 @@ func (ws *WebServer) configureStatic() { | ||||
| 		rw.WriteHeader(200) | ||||
| 		rw.Write(staticWeb.SecurityTxt) | ||||
| 	}) | ||||
| 	// Media files, always local | ||||
| 	ws.lh.PathPrefix("/media").Handler(http.StripPrefix("/media", http.FileServer(http.Dir(config.G.Paths.Media)))) | ||||
| } | ||||
|  | ||||
| func (ws *WebServer) staticHeaderMiddleware(h http.Handler) http.Handler { | ||||
|  | ||||
| @ -98,20 +98,15 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if *u.IsActive { | ||||
| 		attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}}) | ||||
| 	} else { | ||||
| 		attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}}) | ||||
| 	} | ||||
|  | ||||
| 	if u.IsSuperuser { | ||||
| 		attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}}) | ||||
| 	} else { | ||||
| 		attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}}) | ||||
| 	} | ||||
|  | ||||
| 	attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)}) | ||||
|  | ||||
| 	// Old fields for backwards compatibility | ||||
| 	attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{BoolToString(*u.IsActive)}}) | ||||
| 	attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{BoolToString(u.IsSuperuser)}}) | ||||
|  | ||||
| 	attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/active", Values: []string{BoolToString(*u.IsActive)}}) | ||||
| 	attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(u.IsSuperuser)}}) | ||||
|  | ||||
| 	attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...) | ||||
|  | ||||
| 	dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN) | ||||
|  | ||||
| @ -7,6 +7,13 @@ import ( | ||||
| 	"goauthentik.io/outpost/api" | ||||
| ) | ||||
|  | ||||
| func BoolToString(in bool) string { | ||||
| 	if in { | ||||
| 		return "true" | ||||
| 	} | ||||
| 	return "false" | ||||
| } | ||||
|  | ||||
| func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute { | ||||
| 	attrList := []*ldap.EntryAttribute{} | ||||
| 	a := attrs.(*map[string]interface{}) | ||||
| @ -17,6 +24,8 @@ func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute { | ||||
| 			entry.Values = t | ||||
| 		case string: | ||||
| 			entry.Values = []string{t} | ||||
| 		case bool: | ||||
| 			entry.Values = []string{BoolToString(t)} | ||||
| 		} | ||||
| 		attrList = append(attrList, entry) | ||||
| 	} | ||||
|  | ||||
| @ -29,9 +29,10 @@ func (s *Server) bundleProviders(providers []api.ProxyOutpostConfig) []*provider | ||||
| 			log.WithError(err).Warning("Failed to parse URL, skipping provider") | ||||
| 		} | ||||
| 		bundles[idx] = &providerBundle{ | ||||
| 			s:    s, | ||||
| 			Host: externalHost.Host, | ||||
| 			log:  log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name), | ||||
| 			s:             s, | ||||
| 			Host:          externalHost.Host, | ||||
| 			log:           log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name), | ||||
| 			endSessionUrl: provider.OidcConfiguration.EndSessionEndpoint, | ||||
| 		} | ||||
| 		bundles[idx].Build(provider) | ||||
| 	} | ||||
|  | ||||
| @ -25,6 +25,8 @@ type providerBundle struct { | ||||
| 	proxy *OAuthProxy | ||||
| 	Host  string | ||||
|  | ||||
| 	endSessionUrl string | ||||
|  | ||||
| 	cert *tls.Certificate | ||||
|  | ||||
| 	log *log.Entry | ||||
| @ -58,6 +60,8 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options. | ||||
| 	providerOpts.RedeemURL = provider.OidcConfiguration.TokenEndpoint | ||||
| 	providerOpts.OIDCJwksURL = provider.OidcConfiguration.JwksUri | ||||
| 	providerOpts.ProfileURL = provider.OidcConfiguration.UserinfoEndpoint | ||||
| 	providerOpts.ValidateURL = provider.OidcConfiguration.UserinfoEndpoint | ||||
| 	providerOpts.AcrValues = "goauthentik.io/providers/oauth2/default" | ||||
|  | ||||
| 	if *provider.SkipPathRegex != "" { | ||||
| 		skipRegexes := strings.Split(*provider.SkipPathRegex, "\n") | ||||
| @ -153,6 +157,7 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) { | ||||
| 		oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute | ||||
| 	} | ||||
|  | ||||
| 	oauthproxy.endSessionEndpoint = pb.endSessionUrl | ||||
| 	oauthproxy.ExternalHost = pb.Host | ||||
|  | ||||
| 	pb.proxy = oauthproxy | ||||
|  | ||||
| @ -65,31 +65,33 @@ type OAuthProxy struct { | ||||
| 	AuthOnlyPath      string | ||||
| 	UserInfoPath      string | ||||
|  | ||||
| 	endSessionEndpoint         string | ||||
| 	mode                       api.ProxyMode | ||||
| 	redirectURL                *url.URL // the url to receive requests at | ||||
| 	whitelistDomains           []string | ||||
| 	provider                   providers.Provider | ||||
| 	sessionStore               sessionsapi.SessionStore | ||||
| 	ProxyPrefix                string | ||||
| 	serveMux                   http.Handler | ||||
| 	SetXAuthRequest            bool | ||||
| 	SetBasicAuth               bool | ||||
| 	PassUserHeaders            bool | ||||
| 	BasicAuthUserAttribute     string | ||||
| 	BasicAuthPasswordAttribute string | ||||
| 	ExternalHost               string | ||||
| 	PassAccessToken            bool | ||||
| 	SetAuthorization           bool | ||||
| 	PassAuthorization          bool | ||||
| 	PreferEmailToUser          bool | ||||
| 	skipAuthRegex              []string | ||||
| 	skipAuthPreflight          bool | ||||
| 	skipAuthStripHeaders       bool | ||||
| 	mainJwtBearerVerifier      *oidc.IDTokenVerifier | ||||
| 	extraJwtBearerVerifiers    []*oidc.IDTokenVerifier | ||||
| 	compiledRegex              []*regexp.Regexp | ||||
| 	templates                  *template.Template | ||||
| 	realClientIPParser         ipapi.RealClientIPParser | ||||
|  | ||||
| 	redirectURL             *url.URL // the url to receive requests at | ||||
| 	whitelistDomains        []string | ||||
| 	provider                providers.Provider | ||||
| 	sessionStore            sessionsapi.SessionStore | ||||
| 	ProxyPrefix             string | ||||
| 	serveMux                http.Handler | ||||
| 	SetXAuthRequest         bool | ||||
| 	SetBasicAuth            bool | ||||
| 	PassUserHeaders         bool | ||||
| 	PassAccessToken         bool | ||||
| 	SetAuthorization        bool | ||||
| 	PassAuthorization       bool | ||||
| 	PreferEmailToUser       bool | ||||
| 	skipAuthRegex           []string | ||||
| 	skipAuthPreflight       bool | ||||
| 	skipAuthStripHeaders    bool | ||||
| 	mainJwtBearerVerifier   *oidc.IDTokenVerifier | ||||
| 	extraJwtBearerVerifiers []*oidc.IDTokenVerifier | ||||
| 	compiledRegex           []*regexp.Regexp | ||||
| 	templates               *template.Template | ||||
| 	realClientIPParser      ipapi.RealClientIPParser | ||||
|  | ||||
| 	sessionChain alice.Chain | ||||
|  | ||||
| @ -285,19 +287,13 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { | ||||
|  | ||||
| // SignOut sends a response to clear the authentication cookie | ||||
| func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { | ||||
| 	redirect, err := p.GetRedirect(req) | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error obtaining redirect: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	err = p.ClearSessionCookie(rw, req) | ||||
| 	err := p.ClearSessionCookie(rw, req) | ||||
| 	if err != nil { | ||||
| 		p.logger.Errorf("Error clearing session cookie: %v", err) | ||||
| 		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	http.Redirect(rw, req, redirect, http.StatusFound) | ||||
| 	http.Redirect(rw, req, p.endSessionEndpoint, http.StatusFound) | ||||
| } | ||||
|  | ||||
| // AuthenticateOnly checks whether the user is currently logged in | ||||
|  | ||||
| @ -5,7 +5,7 @@ import ( | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| const VERSION = "2021.6.2" | ||||
| const VERSION = "2021.6.4" | ||||
|  | ||||
| func BUILD() string { | ||||
| 	build := os.Getenv("GIT_BUILD_HASH") | ||||
|  | ||||
							
								
								
									
										292
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										292
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2021.6.1 | ||||
|   version: 2021.6.4 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     email: hello@beryju.org | ||||
| @ -3096,7 +3096,11 @@ paths: | ||||
|                 $ref: '#/components/schemas/Link' | ||||
|           description: '' | ||||
|         '404': | ||||
|           description: No recovery flow found. | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Link' | ||||
|           description: '' | ||||
|         '400': | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
| @ -3572,6 +3576,37 @@ paths: | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
|           $ref: '#/components/schemas/GenericError' | ||||
|     post: | ||||
|       operationId: events_events_create | ||||
|       description: Event Read-Only Viewset | ||||
|       tags: | ||||
|       - events | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/EventRequest' | ||||
|           application/x-www-form-urlencoded: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/EventRequest' | ||||
|           multipart/form-data: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/EventRequest' | ||||
|         required: true | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       - cookieAuth: [] | ||||
|       responses: | ||||
|         '201': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Event' | ||||
|           description: '' | ||||
|         '400': | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
|           $ref: '#/components/schemas/GenericError' | ||||
|   /api/v2beta/events/events/{event_uuid}/: | ||||
|     get: | ||||
|       operationId: events_events_retrieve | ||||
| @ -3600,6 +3635,106 @@ paths: | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
|           $ref: '#/components/schemas/GenericError' | ||||
|     put: | ||||
|       operationId: events_events_update | ||||
|       description: Event Read-Only Viewset | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: event_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Event. | ||||
|         required: true | ||||
|       tags: | ||||
|       - events | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/EventRequest' | ||||
|           application/x-www-form-urlencoded: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/EventRequest' | ||||
|           multipart/form-data: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/EventRequest' | ||||
|         required: true | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       - cookieAuth: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Event' | ||||
|           description: '' | ||||
|         '400': | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
|           $ref: '#/components/schemas/GenericError' | ||||
|     patch: | ||||
|       operationId: events_events_partial_update | ||||
|       description: Event Read-Only Viewset | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: event_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Event. | ||||
|         required: true | ||||
|       tags: | ||||
|       - events | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/PatchedEventRequest' | ||||
|           application/x-www-form-urlencoded: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/PatchedEventRequest' | ||||
|           multipart/form-data: | ||||
|             schema: | ||||
|               $ref: '#/components/schemas/PatchedEventRequest' | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       - cookieAuth: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Event' | ||||
|           description: '' | ||||
|         '400': | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
|           $ref: '#/components/schemas/GenericError' | ||||
|     delete: | ||||
|       operationId: events_events_destroy | ||||
|       description: Event Read-Only Viewset | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: event_uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Event. | ||||
|         required: true | ||||
|       tags: | ||||
|       - events | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       - cookieAuth: [] | ||||
|       responses: | ||||
|         '204': | ||||
|           description: No response body | ||||
|         '400': | ||||
|           $ref: '#/components/schemas/ValidationError' | ||||
|         '403': | ||||
|           $ref: '#/components/schemas/GenericError' | ||||
|   /api/v2beta/events/events/actions/: | ||||
|     get: | ||||
|       operationId: events_events_actions_list | ||||
| @ -4441,6 +4576,18 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|       - in: query | ||||
|         name: invalid_response_action | ||||
|         schema: | ||||
|           type: string | ||||
|           enum: | ||||
|           - restart | ||||
|           - restart_with_context | ||||
|           - retry | ||||
|         description: Configure how the flow executor should handle an invalid response | ||||
|           to a challenge. RETRY returns the error message and a similar challenge | ||||
|           to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT | ||||
|           restarts the flow while keeping the current context. | ||||
|       - in: query | ||||
|         name: order | ||||
|         schema: | ||||
| @ -18494,7 +18641,10 @@ components: | ||||
|           title: Kp uuid | ||||
|         name: | ||||
|           type: string | ||||
|         fingerprint: | ||||
|         fingerprint_sha256: | ||||
|           type: string | ||||
|           readOnly: true | ||||
|         fingerprint_sha1: | ||||
|           type: string | ||||
|           readOnly: true | ||||
|         cert_expiry: | ||||
| @ -18517,7 +18667,8 @@ components: | ||||
|       - cert_expiry | ||||
|       - cert_subject | ||||
|       - certificate_download_url | ||||
|       - fingerprint | ||||
|       - fingerprint_sha1 | ||||
|       - fingerprint_sha256 | ||||
|       - name | ||||
|       - pk | ||||
|       - private_key_available | ||||
| @ -18759,6 +18910,12 @@ components: | ||||
|             name: Documentation | ||||
|           - href: https://goauthentik.io/ | ||||
|             name: authentik Website | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|         flow_invalidation: | ||||
|           type: string | ||||
|         flow_recovery: | ||||
|           type: string | ||||
|         flow_unenrollment: | ||||
|           type: string | ||||
|       required: | ||||
| @ -19242,7 +19399,7 @@ components: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         action: | ||||
|           type: string | ||||
|           $ref: '#/components/schemas/EventActions' | ||||
|         app: | ||||
|           type: string | ||||
|         context: | ||||
| @ -19266,6 +19423,34 @@ components: | ||||
|       - app | ||||
|       - created | ||||
|       - pk | ||||
|     EventActions: | ||||
|       enum: | ||||
|       - login | ||||
|       - login_failed | ||||
|       - logout | ||||
|       - user_write | ||||
|       - suspicious_request | ||||
|       - password_set | ||||
|       - secret_view | ||||
|       - invitation_used | ||||
|       - authorize_application | ||||
|       - source_linked | ||||
|       - impersonation_started | ||||
|       - impersonation_ended | ||||
|       - policy_execution | ||||
|       - policy_exception | ||||
|       - property_mapping_exception | ||||
|       - system_task_execution | ||||
|       - system_task_exception | ||||
|       - system_exception | ||||
|       - configuration_error | ||||
|       - model_created | ||||
|       - model_updated | ||||
|       - model_deleted | ||||
|       - email_sent | ||||
|       - update_available | ||||
|       - custom_ | ||||
|       type: string | ||||
|     EventMatcherPolicy: | ||||
|       type: object | ||||
|       description: Event Matcher Policy Serializer | ||||
| @ -19296,7 +19481,7 @@ components: | ||||
|           readOnly: true | ||||
|         action: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/EventMatcherPolicyActionEnum' | ||||
|           - $ref: '#/components/schemas/EventActions' | ||||
|           description: Match created events with this action type. When left empty, | ||||
|             all action types will be matched. | ||||
|         client_ip: | ||||
| @ -19314,34 +19499,6 @@ components: | ||||
|       - pk | ||||
|       - verbose_name | ||||
|       - verbose_name_plural | ||||
|     EventMatcherPolicyActionEnum: | ||||
|       enum: | ||||
|       - login | ||||
|       - login_failed | ||||
|       - logout | ||||
|       - user_write | ||||
|       - suspicious_request | ||||
|       - password_set | ||||
|       - secret_view | ||||
|       - invitation_used | ||||
|       - authorize_application | ||||
|       - source_linked | ||||
|       - impersonation_started | ||||
|       - impersonation_ended | ||||
|       - policy_execution | ||||
|       - policy_exception | ||||
|       - property_mapping_exception | ||||
|       - system_task_execution | ||||
|       - system_task_exception | ||||
|       - system_exception | ||||
|       - configuration_error | ||||
|       - model_created | ||||
|       - model_updated | ||||
|       - model_deleted | ||||
|       - email_sent | ||||
|       - update_available | ||||
|       - custom_ | ||||
|       type: string | ||||
|     EventMatcherPolicyRequest: | ||||
|       type: object | ||||
|       description: Event Matcher Policy Serializer | ||||
| @ -19355,7 +19512,7 @@ components: | ||||
|             will be logged. By default, only execution errors are logged. | ||||
|         action: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/EventMatcherPolicyActionEnum' | ||||
|           - $ref: '#/components/schemas/EventActions' | ||||
|           description: Match created events with this action type. When left empty, | ||||
|             all action types will be matched. | ||||
|         client_ip: | ||||
| @ -19375,7 +19532,7 @@ components: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         action: | ||||
|           type: string | ||||
|           $ref: '#/components/schemas/EventActions' | ||||
|         app: | ||||
|           type: string | ||||
|         context: | ||||
| @ -19673,6 +19830,13 @@ components: | ||||
|           minimum: -2147483648 | ||||
|         policy_engine_mode: | ||||
|           $ref: '#/components/schemas/PolicyEngineMode' | ||||
|         invalid_response_action: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/InvalidResponseActionEnum' | ||||
|           description: Configure how the flow executor should handle an invalid response | ||||
|             to a challenge. RETRY returns the error message and a similar challenge | ||||
|             to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT | ||||
|             restarts the flow while keeping the current context. | ||||
|       required: | ||||
|       - order | ||||
|       - pk | ||||
| @ -19703,6 +19867,13 @@ components: | ||||
|           minimum: -2147483648 | ||||
|         policy_engine_mode: | ||||
|           $ref: '#/components/schemas/PolicyEngineMode' | ||||
|         invalid_response_action: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/InvalidResponseActionEnum' | ||||
|           description: Configure how the flow executor should handle an invalid response | ||||
|             to a challenge. RETRY returns the error message and a similar challenge | ||||
|             to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT | ||||
|             restarts the flow while keeping the current context. | ||||
|       required: | ||||
|       - order | ||||
|       - stage | ||||
| @ -20048,6 +20219,12 @@ components: | ||||
|       - api | ||||
|       - recovery | ||||
|       type: string | ||||
|     InvalidResponseActionEnum: | ||||
|       enum: | ||||
|       - retry | ||||
|       - restart | ||||
|       - restart_with_context | ||||
|       type: string | ||||
|     Invitation: | ||||
|       type: object | ||||
|       description: Invitation Serializer | ||||
| @ -24429,7 +24606,7 @@ components: | ||||
|             will be logged. By default, only execution errors are logged. | ||||
|         action: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/EventMatcherPolicyActionEnum' | ||||
|           - $ref: '#/components/schemas/EventActions' | ||||
|           description: Match created events with this action type. When left empty, | ||||
|             all action types will be matched. | ||||
|         client_ip: | ||||
| @ -24441,6 +24618,29 @@ components: | ||||
|           - $ref: '#/components/schemas/AppEnum' | ||||
|           description: Match events created by selected application. When left empty, | ||||
|             all applications are matched. | ||||
|     PatchedEventRequest: | ||||
|       type: object | ||||
|       description: Event Serializer | ||||
|       properties: | ||||
|         user: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         action: | ||||
|           $ref: '#/components/schemas/EventActions' | ||||
|         app: | ||||
|           type: string | ||||
|         context: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|         client_ip: | ||||
|           type: string | ||||
|           nullable: true | ||||
|         expires: | ||||
|           type: string | ||||
|           format: date-time | ||||
|         tenant: | ||||
|           type: object | ||||
|           additionalProperties: {} | ||||
|     PatchedExpressionPolicyRequest: | ||||
|       type: object | ||||
|       description: Group Membership Policy Serializer | ||||
| @ -24502,6 +24702,13 @@ components: | ||||
|           minimum: -2147483648 | ||||
|         policy_engine_mode: | ||||
|           $ref: '#/components/schemas/PolicyEngineMode' | ||||
|         invalid_response_action: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/InvalidResponseActionEnum' | ||||
|           description: Configure how the flow executor should handle an invalid response | ||||
|             to a challenge. RETRY returns the error message and a similar challenge | ||||
|             to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT | ||||
|             restarts the flow while keeping the current context. | ||||
|     PatchedGroupRequest: | ||||
|       type: object | ||||
|       description: Group Serializer | ||||
| @ -25579,6 +25786,9 @@ components: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/FlowRequest' | ||||
|         create_users_as_inactive: | ||||
|           type: boolean | ||||
|           description: When set, newly created users are inactive and cannot login. | ||||
|     PatchedWebAuthnDeviceRequest: | ||||
|       type: object | ||||
|       description: Serializer for WebAuthn authenticator devices | ||||
| @ -26481,6 +26691,8 @@ components: | ||||
|         id_token: | ||||
|           type: string | ||||
|           readOnly: true | ||||
|         revoked: | ||||
|           type: boolean | ||||
|       required: | ||||
|       - id_token | ||||
|       - is_expired | ||||
| @ -28073,6 +28285,9 @@ components: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/Flow' | ||||
|         create_users_as_inactive: | ||||
|           type: boolean | ||||
|           description: When set, newly created users are inactive and cannot login. | ||||
|       required: | ||||
|       - component | ||||
|       - name | ||||
| @ -28089,6 +28304,9 @@ components: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/FlowRequest' | ||||
|         create_users_as_inactive: | ||||
|           type: boolean | ||||
|           description: When set, newly created users are inactive and cannot login. | ||||
|       required: | ||||
|       - name | ||||
|     ValidationError: | ||||
|  | ||||
							
								
								
									
										232
									
								
								tests/e2e/test_provider_ldap.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								tests/e2e/test_provider_ldap.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,232 @@ | ||||
| """LDAP and Outpost e2e tests""" | ||||
| from sys import platform | ||||
| from time import sleep | ||||
| from unittest.case import skipUnless | ||||
|  | ||||
| from docker.client import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from ldap3 import ( | ||||
|     ALL, | ||||
|     ALL_ATTRIBUTES, | ||||
|     ALL_OPERATIONAL_ATTRIBUTES, | ||||
|     SUBTREE, | ||||
|     Connection, | ||||
|     Server, | ||||
| ) | ||||
| from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult | ||||
|  | ||||
| from authentik.core.models import Application, Group, User | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.outposts.models import Outpost, OutpostType | ||||
| from authentik.providers.ldap.models import LDAPProvider | ||||
| from tests.e2e.utils import ( | ||||
|     USER, | ||||
|     SeleniumTestCase, | ||||
|     apply_migration, | ||||
|     object_manager, | ||||
|     retry, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestProviderLDAP(SeleniumTestCase): | ||||
|     """LDAP and Outpost e2e tests""" | ||||
|  | ||||
|     ldap_container: Container | ||||
|  | ||||
|     def tearDown(self) -> None: | ||||
|         super().tearDown() | ||||
|         self.output_container_logs(self.ldap_container) | ||||
|         self.ldap_container.kill() | ||||
|  | ||||
|     def start_ldap(self, outpost: Outpost) -> Container: | ||||
|         """Start ldap container based on outpost created""" | ||||
|         client: DockerClient = from_env() | ||||
|         container = client.containers.run( | ||||
|             image="beryju.org/authentik/outpost-ldap:gh-master", | ||||
|             detach=True, | ||||
|             network_mode="host", | ||||
|             auto_remove=True, | ||||
|             environment={ | ||||
|                 "AUTHENTIK_HOST": self.live_server_url, | ||||
|                 "AUTHENTIK_TOKEN": outpost.token.key, | ||||
|             }, | ||||
|         ) | ||||
|         return container | ||||
|  | ||||
|     def _prepare(self) -> User: | ||||
|         """prepare user, provider, app and container""" | ||||
|         # set additionalHeaders to test later | ||||
|         user = USER() | ||||
|         user.attributes["extraAttribute"] = "bar" | ||||
|         user.save() | ||||
|  | ||||
|         ldap: LDAPProvider = LDAPProvider.objects.create( | ||||
|             name="ldap_provider", | ||||
|             authorization_flow=Flow.objects.get(slug="default-authentication-flow"), | ||||
|             search_group=Group.objects.first(), | ||||
|         ) | ||||
|         # we need to create an application to actually access the ldap | ||||
|         Application.objects.create(name="ldap", slug="ldap", provider=ldap) | ||||
|         outpost: Outpost = Outpost.objects.create( | ||||
|             name="ldap_outpost", | ||||
|             type=OutpostType.LDAP, | ||||
|         ) | ||||
|         outpost.providers.add(ldap) | ||||
|         outpost.save() | ||||
|         user = outpost.user | ||||
|  | ||||
|         self.ldap_container = self.start_ldap(outpost) | ||||
|  | ||||
|         # Wait until outpost healthcheck succeeds | ||||
|         healthcheck_retries = 0 | ||||
|         while healthcheck_retries < 50: | ||||
|             if len(outpost.state) > 0: | ||||
|                 state = outpost.state[0] | ||||
|                 if state.last_seen: | ||||
|                     break | ||||
|             healthcheck_retries += 1 | ||||
|             sleep(0.5) | ||||
|         return user | ||||
|  | ||||
|     @retry() | ||||
|     @apply_migration("authentik_core", "0003_default_user") | ||||
|     @apply_migration("authentik_flows", "0008_default_flows") | ||||
|     @object_manager | ||||
|     def test_ldap_bind_success(self): | ||||
|         """Test simple bind""" | ||||
|         self._prepare() | ||||
|         server = Server("ldap://localhost:3389", get_info=ALL) | ||||
|         _connection = Connection( | ||||
|             server, | ||||
|             raise_exceptions=True, | ||||
|             user=f"cn={USER().username},ou=users,DC=ldap,DC=goauthentik,DC=io", | ||||
|             password=USER().username, | ||||
|         ) | ||||
|         _connection.bind() | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.LOGIN, | ||||
|                 user={ | ||||
|                     "pk": USER().pk, | ||||
|                     "email": USER().email, | ||||
|                     "username": USER().username, | ||||
|                 }, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     @apply_migration("authentik_core", "0003_default_user") | ||||
|     @apply_migration("authentik_flows", "0008_default_flows") | ||||
|     @object_manager | ||||
|     def test_ldap_bind_fail(self): | ||||
|         """Test simple bind (failed)""" | ||||
|         self._prepare() | ||||
|         server = Server("ldap://localhost:3389", get_info=ALL) | ||||
|         _connection = Connection( | ||||
|             server, | ||||
|             raise_exceptions=True, | ||||
|             user=f"cn={USER().username},ou=users,DC=ldap,DC=goauthentik,DC=io", | ||||
|             password=USER().username + "fqwerwqer", | ||||
|         ) | ||||
|         with self.assertRaises(LDAPInsufficientAccessRightsResult): | ||||
|             _connection.bind() | ||||
|         anon = get_anonymous_user() | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.LOGIN_FAILED, | ||||
|                 user={"pk": anon.pk, "email": anon.email, "username": anon.username}, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     @apply_migration("authentik_core", "0003_default_user") | ||||
|     @apply_migration("authentik_core", "0009_group_is_superuser") | ||||
|     @apply_migration("authentik_flows", "0008_default_flows") | ||||
|     @object_manager | ||||
|     def test_ldap_bind_search(self): | ||||
|         """Test simple bind + search""" | ||||
|         outpost_user = self._prepare() | ||||
|         server = Server("ldap://localhost:3389", get_info=ALL) | ||||
|         _connection = Connection( | ||||
|             server, | ||||
|             raise_exceptions=True, | ||||
|             user=f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io", | ||||
|             password=USER().username, | ||||
|         ) | ||||
|         _connection.bind() | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.LOGIN, | ||||
|                 user={ | ||||
|                     "pk": USER().pk, | ||||
|                     "email": USER().email, | ||||
|                     "username": USER().username, | ||||
|                 }, | ||||
|             ) | ||||
|         ) | ||||
|         _connection.search( | ||||
|             "ou=users,dc=ldap,dc=goauthentik,dc=io", | ||||
|             "(objectClass=user)", | ||||
|             search_scope=SUBTREE, | ||||
|             attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES], | ||||
|         ) | ||||
|         response = _connection.response | ||||
|         # Remove raw_attributes to make checking easier | ||||
|         for obj in response: | ||||
|             del obj["raw_attributes"] | ||||
|             del obj["raw_dn"] | ||||
|         self.assertCountEqual( | ||||
|             response, | ||||
|             [ | ||||
|                 { | ||||
|                     "dn": f"cn={outpost_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", | ||||
|                     "attributes": { | ||||
|                         "cn": [outpost_user.username], | ||||
|                         "uid": [outpost_user.uid], | ||||
|                         "name": [""], | ||||
|                         "displayName": [""], | ||||
|                         "mail": [""], | ||||
|                         "objectClass": [ | ||||
|                             "user", | ||||
|                             "organizationalPerson", | ||||
|                             "goauthentik.io/ldap/user", | ||||
|                         ], | ||||
|                         "memberOf": [], | ||||
|                         "accountStatus": ["true"], | ||||
|                         "superuser": ["false"], | ||||
|                         "goauthentik.io/ldap/active": ["true"], | ||||
|                         "goauthentik.io/ldap/superuser": ["false"], | ||||
|                         "goauthentik.io/user/override-ips": ["true"], | ||||
|                         "goauthentik.io/user/service-account": ["true"], | ||||
|                     }, | ||||
|                     "type": "searchResEntry", | ||||
|                 }, | ||||
|                 { | ||||
|                     "dn": f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io", | ||||
|                     "attributes": { | ||||
|                         "cn": [USER().username], | ||||
|                         "uid": [USER().uid], | ||||
|                         "name": [USER().name], | ||||
|                         "displayName": [USER().name], | ||||
|                         "mail": [USER().email], | ||||
|                         "objectClass": [ | ||||
|                             "user", | ||||
|                             "organizationalPerson", | ||||
|                             "goauthentik.io/ldap/user", | ||||
|                         ], | ||||
|                         "memberOf": [ | ||||
|                             "cn=authentik Admins,ou=groups,dc=ldap,dc=goauthentik,dc=io" | ||||
|                         ], | ||||
|                         "accountStatus": ["true"], | ||||
|                         "superuser": ["true"], | ||||
|                         "goauthentik.io/ldap/active": ["true"], | ||||
|                         "goauthentik.io/ldap/superuser": ["true"], | ||||
|                         "extraAttribute": ["bar"], | ||||
|                     }, | ||||
|                     "type": "searchResEntry", | ||||
|                 }, | ||||
|             ], | ||||
|         ) | ||||
| @ -119,6 +119,13 @@ class TestProviderProxy(SeleniumTestCase): | ||||
|         self.assertIn("X-Forwarded-Preferred-Username: akadmin", full_body_text) | ||||
|         self.assertIn("X-Foo: bar", full_body_text) | ||||
|  | ||||
|         self.driver.get("http://localhost:4180/akprox/sign_out") | ||||
|         sleep(2) | ||||
|         full_body_text = self.driver.find_element( | ||||
|             By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl" | ||||
|         ).text | ||||
|         self.assertIn("You've logged out of proxy.", full_body_text) | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestProviderProxyConnect(ChannelsLiveServerTestCase): | ||||
|  | ||||
| @ -62,11 +62,14 @@ class OutpostDockerTests(TestCase): | ||||
|         ) | ||||
|         authentication_kp = CertificateKeyPair.objects.create( | ||||
|             name="docker-authentication", | ||||
|             # pylint: disable=consider-using-with | ||||
|             certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(), | ||||
|             # pylint: disable=consider-using-with | ||||
|             key_data=open(f"{self.ssl_folder}/client/key.pem").read(), | ||||
|         ) | ||||
|         verification_kp = CertificateKeyPair.objects.create( | ||||
|             name="docker-verification", | ||||
|             # pylint: disable=consider-using-with | ||||
|             certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(), | ||||
|         ) | ||||
|         self.service_connection = DockerServiceConnection.objects.create( | ||||
|  | ||||
| @ -62,11 +62,14 @@ class TestProxyDocker(TestCase): | ||||
|         ) | ||||
|         authentication_kp = CertificateKeyPair.objects.create( | ||||
|             name="docker-authentication", | ||||
|             # pylint: disable=consider-using-with | ||||
|             certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(), | ||||
|             # pylint: disable=consider-using-with | ||||
|             key_data=open(f"{self.ssl_folder}/client/key.pem").read(), | ||||
|         ) | ||||
|         verification_kp = CertificateKeyPair.objects.create( | ||||
|             name="docker-verification", | ||||
|             # pylint: disable=consider-using-with | ||||
|             certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(), | ||||
|         ) | ||||
|         self.service_connection = DockerServiceConnection.objects.create( | ||||
|  | ||||
							
								
								
									
										436
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										436
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -18,28 +18,28 @@ | ||||
|                 "@lingui/cli": "^3.10.2", | ||||
|                 "@lingui/core": "^3.10.4", | ||||
|                 "@lingui/macro": "^3.10.2", | ||||
|                 "@patternfly/patternfly": "^4.108.2", | ||||
|                 "@patternfly/patternfly": "^4.115.2", | ||||
|                 "@polymer/iron-form": "^3.0.1", | ||||
|                 "@polymer/paper-input": "^3.2.1", | ||||
|                 "@rollup/plugin-babel": "^5.3.0", | ||||
|                 "@rollup/plugin-replace": "^2.4.2", | ||||
|                 "@rollup/plugin-typescript": "^8.2.1", | ||||
|                 "@sentry/browser": "^6.7.2", | ||||
|                 "@sentry/tracing": "^6.7.2", | ||||
|                 "@types/chart.js": "^2.9.32", | ||||
|                 "@types/codemirror": "5.60.0", | ||||
|                 "@sentry/browser": "^6.8.0", | ||||
|                 "@sentry/tracing": "^6.8.0", | ||||
|                 "@types/chart.js": "^2.9.33", | ||||
|                 "@types/codemirror": "5.60.1", | ||||
|                 "@types/grecaptcha": "^3.0.2", | ||||
|                 "@typescript-eslint/eslint-plugin": "^4.28.0", | ||||
|                 "@typescript-eslint/parser": "^4.28.0", | ||||
|                 "@typescript-eslint/eslint-plugin": "^4.28.1", | ||||
|                 "@typescript-eslint/parser": "^4.28.1", | ||||
|                 "@webcomponents/webcomponentsjs": "^2.5.0", | ||||
|                 "authentik-api": "file:api", | ||||
|                 "babel-plugin-macros": "^3.1.0", | ||||
|                 "base64-js": "^1.5.1", | ||||
|                 "chart.js": "^3.3.2", | ||||
|                 "chart.js": "^3.4.1", | ||||
|                 "chartjs-adapter-moment": "^1.0.0", | ||||
|                 "codemirror": "^5.62.0", | ||||
|                 "construct-style-sheets-polyfill": "^2.4.16", | ||||
|                 "eslint": "^7.29.0", | ||||
|                 "eslint": "^7.30.0", | ||||
|                 "eslint-config-google": "^0.14.0", | ||||
|                 "eslint-plugin-custom-elements": "0.0.2", | ||||
|                 "eslint-plugin-lit": "^1.5.1", | ||||
| @ -48,7 +48,7 @@ | ||||
|                 "lit-html": "^1.4.1", | ||||
|                 "moment": "^2.29.1", | ||||
|                 "rapidoc": "^9.0.0", | ||||
|                 "rollup": "^2.52.2", | ||||
|                 "rollup": "^2.52.7", | ||||
|                 "rollup-plugin-commonjs": "^10.1.0", | ||||
|                 "rollup-plugin-copy": "^3.4.0", | ||||
|                 "rollup-plugin-cssimport": "^1.0.2", | ||||
| @ -58,15 +58,16 @@ | ||||
|                 "rollup-plugin-terser": "^7.0.2", | ||||
|                 "ts-lit-plugin": "^1.2.1", | ||||
|                 "tslib": "^2.3.0", | ||||
|                 "typescript": "^4.3.4", | ||||
|                 "typescript": "^4.3.5", | ||||
|                 "webcomponent-qr-code": "^1.0.5", | ||||
|                 "yaml": "^1.10.2" | ||||
|             } | ||||
|             }, | ||||
|             "devDependencies": {} | ||||
|         }, | ||||
|         "api": { | ||||
|             "name": "authentik-api", | ||||
|             "version": "0.0.1", | ||||
|             "dependencies": { | ||||
|             "version": "1.0.0", | ||||
|             "devDependencies": { | ||||
|                 "typescript": "^3.6" | ||||
|             } | ||||
|         }, | ||||
| @ -74,6 +75,7 @@ | ||||
|             "version": "3.9.9", | ||||
|             "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", | ||||
|             "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", | ||||
|             "dev": true, | ||||
|             "bin": { | ||||
|                 "tsc": "bin/tsc", | ||||
|                 "tsserver": "bin/tsserver" | ||||
| @ -1739,6 +1741,24 @@ | ||||
|                 "node": ">=6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@humanwhocodes/config-array": { | ||||
|             "version": "0.5.0", | ||||
|             "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", | ||||
|             "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", | ||||
|             "dependencies": { | ||||
|                 "@humanwhocodes/object-schema": "^1.2.0", | ||||
|                 "debug": "^4.1.1", | ||||
|                 "minimatch": "^3.0.4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=10.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@humanwhocodes/object-schema": { | ||||
|             "version": "1.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", | ||||
|             "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==" | ||||
|         }, | ||||
|         "node_modules/@jest/types": { | ||||
|             "version": "26.6.2", | ||||
|             "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", | ||||
| @ -2120,9 +2140,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@patternfly/patternfly": { | ||||
|             "version": "4.108.2", | ||||
|             "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.108.2.tgz", | ||||
|             "integrity": "sha512-z0VB+1CXcH+eoClYQABwapX5FURSvm1nPr6asLWwg/Z4Wuxs0RjZpC6Gb+KRm8nGQwSAcMKZY1jLfPqVnznQnw==" | ||||
|             "version": "4.115.2", | ||||
|             "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.115.2.tgz", | ||||
|             "integrity": "sha512-7hbJ4pRmj+rlXclD2F/UwceO6fS+9flGsgHc4eUc7NyTN2GXl6PLcqrjE2CtiKEPV90+KwsGQGJXZj8bz9HweA==" | ||||
|         }, | ||||
|         "node_modules/@polymer/font-roboto": { | ||||
|             "version": "3.0.2", | ||||
| @ -2314,13 +2334,13 @@ | ||||
|             "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" | ||||
|         }, | ||||
|         "node_modules/@sentry/browser": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.7.2.tgz", | ||||
|             "integrity": "sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.8.0.tgz", | ||||
|             "integrity": "sha512-nxa71csHlG5sMHUxI4e4xxuCWtbCv/QbBfMsYw7ncJSfCKG3yNlCVh8NJ7NS0rZW/MJUT6S6+r93zw0HetNDOA==", | ||||
|             "dependencies": { | ||||
|                 "@sentry/core": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/core": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2333,14 +2353,14 @@ | ||||
|             "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         }, | ||||
|         "node_modules/@sentry/core": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.7.2.tgz", | ||||
|             "integrity": "sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.8.0.tgz", | ||||
|             "integrity": "sha512-vJzWt/znEB+JqVwtwfjkRrAYRN+ep+l070Ti8GhJnvwU4IDtVlV3T/jVNrj6rl6UChcczaJQMxVxtG5x0crlAA==", | ||||
|             "dependencies": { | ||||
|                 "@sentry/hub": "6.7.2", | ||||
|                 "@sentry/minimal": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/hub": "6.8.0", | ||||
|                 "@sentry/minimal": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2353,12 +2373,12 @@ | ||||
|             "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         }, | ||||
|         "node_modules/@sentry/hub": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.7.2.tgz", | ||||
|             "integrity": "sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.8.0.tgz", | ||||
|             "integrity": "sha512-hFrI2Ss1fTov7CH64FJpigqRxH7YvSnGeqxT9Jc1BL7nzW/vgCK+Oh2mOZbosTcrzoDv+lE8ViOnSN3w/fo+rg==", | ||||
|             "dependencies": { | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2371,12 +2391,12 @@ | ||||
|             "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         }, | ||||
|         "node_modules/@sentry/minimal": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.7.2.tgz", | ||||
|             "integrity": "sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.8.0.tgz", | ||||
|             "integrity": "sha512-MRxUKXiiYwKjp8mOQMpTpEuIby1Jh3zRTU0cmGZtfsZ38BC1JOle8xlwC4FdtOH+VvjSYnPBMya5lgNHNPUJDQ==", | ||||
|             "dependencies": { | ||||
|                 "@sentry/hub": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/hub": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2389,14 +2409,14 @@ | ||||
|             "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         }, | ||||
|         "node_modules/@sentry/tracing": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.7.2.tgz", | ||||
|             "integrity": "sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.8.0.tgz", | ||||
|             "integrity": "sha512-3gDkQnmOuOjHz5rY7BOatLEUksANU3efR8wuBa2ujsPQvoLSLFuyZpRjPPsxuUHQOqAYIbSNAoDloXECvQeHjw==", | ||||
|             "dependencies": { | ||||
|                 "@sentry/hub": "6.7.2", | ||||
|                 "@sentry/minimal": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/hub": "6.8.0", | ||||
|                 "@sentry/minimal": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2409,19 +2429,19 @@ | ||||
|             "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         }, | ||||
|         "node_modules/@sentry/types": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.7.2.tgz", | ||||
|             "integrity": "sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.8.0.tgz", | ||||
|             "integrity": "sha512-PbSxqlh6Fd5thNU5f8EVYBVvX+G7XdPA+ThNb2QvSK8yv3rIf0McHTyF6sIebgJ38OYN7ZFK7vvhC/RgSAfYTA==", | ||||
|             "engines": { | ||||
|                 "node": ">=6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@sentry/utils": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.7.2.tgz", | ||||
|             "integrity": "sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.8.0.tgz", | ||||
|             "integrity": "sha512-OYlI2JNrcWKMdvYbWNdQwR4QBVv2V0y5wK0U6f53nArv6RsyO5TzwRu5rMVSIZofUUqjoE5hl27jqnR+vpUrsA==", | ||||
|             "dependencies": { | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2434,9 +2454,9 @@ | ||||
|             "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | ||||
|         }, | ||||
|         "node_modules/@types/chart.js": { | ||||
|             "version": "2.9.32", | ||||
|             "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz", | ||||
|             "integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==", | ||||
|             "version": "2.9.33", | ||||
|             "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz", | ||||
|             "integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==", | ||||
|             "dependencies": { | ||||
|                 "moment": "^2.10.2" | ||||
|             } | ||||
| @ -2451,9 +2471,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/codemirror": { | ||||
|             "version": "5.60.0", | ||||
|             "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz", | ||||
|             "integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==", | ||||
|             "version": "5.60.1", | ||||
|             "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.1.tgz", | ||||
|             "integrity": "sha512-yV14LQ5VvghnW0uSuCw2bEfZC6NvxHQEckl2w3dEk5l0yPGzQh14dCaWvG5KD/2l3cgFSifR+6nIUD7LDLdUTg==", | ||||
|             "dependencies": { | ||||
|                 "@types/tern": "*" | ||||
|             } | ||||
| @ -2579,12 +2599,12 @@ | ||||
|             "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/eslint-plugin": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz", | ||||
|             "integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz", | ||||
|             "integrity": "sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ==", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/experimental-utils": "4.28.0", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.0", | ||||
|                 "@typescript-eslint/experimental-utils": "4.28.1", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.1", | ||||
|                 "debug": "^4.3.1", | ||||
|                 "functional-red-black-tree": "^1.0.1", | ||||
|                 "regexpp": "^3.1.0", | ||||
| @ -2609,14 +2629,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/experimental-utils": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz", | ||||
|             "integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz", | ||||
|             "integrity": "sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q==", | ||||
|             "dependencies": { | ||||
|                 "@types/json-schema": "^7.0.7", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.0", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.1", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.1", | ||||
|                 "eslint-scope": "^5.1.1", | ||||
|                 "eslint-utils": "^3.0.0" | ||||
|             }, | ||||
| @ -2649,13 +2669,13 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/parser": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz", | ||||
|             "integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.1.tgz", | ||||
|             "integrity": "sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/scope-manager": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.0", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.1", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.1", | ||||
|                 "debug": "^4.3.1" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -2675,12 +2695,12 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/scope-manager": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz", | ||||
|             "integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz", | ||||
|             "integrity": "sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.0" | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": "^8.10.0 || ^10.13.0 || >=11.10.1" | ||||
| @ -2691,9 +2711,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/types": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz", | ||||
|             "integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.1.tgz", | ||||
|             "integrity": "sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg==", | ||||
|             "engines": { | ||||
|                 "node": "^8.10.0 || ^10.13.0 || >=11.10.1" | ||||
|             }, | ||||
| @ -2703,12 +2723,12 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/typescript-estree": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz", | ||||
|             "integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz", | ||||
|             "integrity": "sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.1", | ||||
|                 "debug": "^4.3.1", | ||||
|                 "globby": "^11.0.3", | ||||
|                 "is-glob": "^4.0.1", | ||||
| @ -2748,11 +2768,11 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@typescript-eslint/visitor-keys": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz", | ||||
|             "integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz", | ||||
|             "integrity": "sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==", | ||||
|             "dependencies": { | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "eslint-visitor-keys": "^2.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -3316,9 +3336,9 @@ | ||||
|             "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" | ||||
|         }, | ||||
|         "node_modules/chart.js": { | ||||
|             "version": "3.3.2", | ||||
|             "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz", | ||||
|             "integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA==" | ||||
|             "version": "3.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz", | ||||
|             "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g==" | ||||
|         }, | ||||
|         "node_modules/chartjs-adapter-moment": { | ||||
|             "version": "1.0.0", | ||||
| @ -3861,12 +3881,13 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/eslint": { | ||||
|             "version": "7.29.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz", | ||||
|             "integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==", | ||||
|             "version": "7.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", | ||||
|             "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", | ||||
|             "dependencies": { | ||||
|                 "@babel/code-frame": "7.12.11", | ||||
|                 "@eslint/eslintrc": "^0.4.2", | ||||
|                 "@humanwhocodes/config-array": "^0.5.0", | ||||
|                 "ajv": "^6.10.0", | ||||
|                 "chalk": "^4.0.0", | ||||
|                 "cross-spawn": "^7.0.2", | ||||
| @ -6770,9 +6791,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/rollup": { | ||||
|             "version": "2.52.2", | ||||
|             "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz", | ||||
|             "integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==", | ||||
|             "version": "2.52.7", | ||||
|             "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.7.tgz", | ||||
|             "integrity": "sha512-55cSH4CCU6MaPr9TAOyrIC+7qFCHscL7tkNsm1MBfIJRRqRbCEY0mmeFn4Wg8FKsHtEH8r389Fz38r/o+kgXLg==", | ||||
|             "bin": { | ||||
|                 "rollup": "dist/bin/rollup" | ||||
|             }, | ||||
| @ -7604,9 +7625,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/typescript": { | ||||
|             "version": "4.3.4", | ||||
|             "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", | ||||
|             "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", | ||||
|             "version": "4.3.5", | ||||
|             "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", | ||||
|             "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", | ||||
|             "bin": { | ||||
|                 "tsc": "bin/tsc", | ||||
|                 "tsserver": "bin/tsserver" | ||||
| @ -9191,6 +9212,21 @@ | ||||
|             "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", | ||||
|             "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" | ||||
|         }, | ||||
|         "@humanwhocodes/config-array": { | ||||
|             "version": "0.5.0", | ||||
|             "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", | ||||
|             "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", | ||||
|             "requires": { | ||||
|                 "@humanwhocodes/object-schema": "^1.2.0", | ||||
|                 "debug": "^4.1.1", | ||||
|                 "minimatch": "^3.0.4" | ||||
|             } | ||||
|         }, | ||||
|         "@humanwhocodes/object-schema": { | ||||
|             "version": "1.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", | ||||
|             "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==" | ||||
|         }, | ||||
|         "@jest/types": { | ||||
|             "version": "26.6.2", | ||||
|             "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", | ||||
| @ -9482,9 +9518,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@patternfly/patternfly": { | ||||
|             "version": "4.108.2", | ||||
|             "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.108.2.tgz", | ||||
|             "integrity": "sha512-z0VB+1CXcH+eoClYQABwapX5FURSvm1nPr6asLWwg/Z4Wuxs0RjZpC6Gb+KRm8nGQwSAcMKZY1jLfPqVnznQnw==" | ||||
|             "version": "4.115.2", | ||||
|             "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.115.2.tgz", | ||||
|             "integrity": "sha512-7hbJ4pRmj+rlXclD2F/UwceO6fS+9flGsgHc4eUc7NyTN2GXl6PLcqrjE2CtiKEPV90+KwsGQGJXZj8bz9HweA==" | ||||
|         }, | ||||
|         "@polymer/font-roboto": { | ||||
|             "version": "3.0.2", | ||||
| @ -9669,13 +9705,13 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@sentry/browser": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.7.2.tgz", | ||||
|             "integrity": "sha512-Lv0Ne1QcesyGAhVcQDfQa3hDPR/MhPSDTMg3xFi+LxqztchVc4w/ynzR0wCZFb8KIHpTj5SpJHfxpDhXYMtS9g==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.8.0.tgz", | ||||
|             "integrity": "sha512-nxa71csHlG5sMHUxI4e4xxuCWtbCv/QbBfMsYw7ncJSfCKG3yNlCVh8NJ7NS0rZW/MJUT6S6+r93zw0HetNDOA==", | ||||
|             "requires": { | ||||
|                 "@sentry/core": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/core": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "dependencies": { | ||||
| @ -9687,14 +9723,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@sentry/core": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.7.2.tgz", | ||||
|             "integrity": "sha512-NTZqwN5nR94yrXmSfekoPs1mIFuKvf8esdIW/DadwSKWAdLJwQTJY9xK/8PQv+SEzd7wiitPAx+mCw2By1xiNQ==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.8.0.tgz", | ||||
|             "integrity": "sha512-vJzWt/znEB+JqVwtwfjkRrAYRN+ep+l070Ti8GhJnvwU4IDtVlV3T/jVNrj6rl6UChcczaJQMxVxtG5x0crlAA==", | ||||
|             "requires": { | ||||
|                 "@sentry/hub": "6.7.2", | ||||
|                 "@sentry/minimal": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/hub": "6.8.0", | ||||
|                 "@sentry/minimal": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "dependencies": { | ||||
| @ -9706,12 +9742,12 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@sentry/hub": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.7.2.tgz", | ||||
|             "integrity": "sha512-05qVW6ymChJsXag4+fYCQokW3AcABIgcqrVYZUBf6GMU/Gbz5SJqpV7y1+njwWvnPZydMncP9LaDVpMKbE7UYQ==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.8.0.tgz", | ||||
|             "integrity": "sha512-hFrI2Ss1fTov7CH64FJpigqRxH7YvSnGeqxT9Jc1BL7nzW/vgCK+Oh2mOZbosTcrzoDv+lE8ViOnSN3w/fo+rg==", | ||||
|             "requires": { | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "dependencies": { | ||||
| @ -9723,12 +9759,12 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@sentry/minimal": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.7.2.tgz", | ||||
|             "integrity": "sha512-jkpwFv2GFHoVl5vnK+9/Q+Ea8eVdbJ3hn3/Dqq9MOLFnVK7ED6MhdHKLT79puGSFj+85OuhM5m2Q44mIhyS5mw==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.8.0.tgz", | ||||
|             "integrity": "sha512-MRxUKXiiYwKjp8mOQMpTpEuIby1Jh3zRTU0cmGZtfsZ38BC1JOle8xlwC4FdtOH+VvjSYnPBMya5lgNHNPUJDQ==", | ||||
|             "requires": { | ||||
|                 "@sentry/hub": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/hub": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "dependencies": { | ||||
| @ -9740,14 +9776,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@sentry/tracing": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.7.2.tgz", | ||||
|             "integrity": "sha512-juKlI7FICKONWJFJxDxerj0A+8mNRhmtrdR+OXFqOkqSAy/QXlSFZcA/j//O19k2CfwK1BrvoMcQ/4gnffUOVg==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.8.0.tgz", | ||||
|             "integrity": "sha512-3gDkQnmOuOjHz5rY7BOatLEUksANU3efR8wuBa2ujsPQvoLSLFuyZpRjPPsxuUHQOqAYIbSNAoDloXECvQeHjw==", | ||||
|             "requires": { | ||||
|                 "@sentry/hub": "6.7.2", | ||||
|                 "@sentry/minimal": "6.7.2", | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/utils": "6.7.2", | ||||
|                 "@sentry/hub": "6.8.0", | ||||
|                 "@sentry/minimal": "6.8.0", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "@sentry/utils": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "dependencies": { | ||||
| @ -9759,16 +9795,16 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@sentry/types": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.7.2.tgz", | ||||
|             "integrity": "sha512-h21Go/PfstUN+ZV6SbwRSZVg9GXRJWdLfHoO5PSVb3TVEMckuxk8tAE57/u+UZDwX8wu+Xyon2TgsKpiWKxqUg==" | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.8.0.tgz", | ||||
|             "integrity": "sha512-PbSxqlh6Fd5thNU5f8EVYBVvX+G7XdPA+ThNb2QvSK8yv3rIf0McHTyF6sIebgJ38OYN7ZFK7vvhC/RgSAfYTA==" | ||||
|         }, | ||||
|         "@sentry/utils": { | ||||
|             "version": "6.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.7.2.tgz", | ||||
|             "integrity": "sha512-9COL7aaBbe61Hp5BlArtXZ1o/cxli1NGONLPrVT4fMyeQFmLonhUiy77NdsW19XnvhvaA+2IoV5dg3dnFiF/og==", | ||||
|             "version": "6.8.0", | ||||
|             "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.8.0.tgz", | ||||
|             "integrity": "sha512-OYlI2JNrcWKMdvYbWNdQwR4QBVv2V0y5wK0U6f53nArv6RsyO5TzwRu5rMVSIZofUUqjoE5hl27jqnR+vpUrsA==", | ||||
|             "requires": { | ||||
|                 "@sentry/types": "6.7.2", | ||||
|                 "@sentry/types": "6.8.0", | ||||
|                 "tslib": "^1.9.3" | ||||
|             }, | ||||
|             "dependencies": { | ||||
| @ -9780,9 +9816,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@types/chart.js": { | ||||
|             "version": "2.9.32", | ||||
|             "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz", | ||||
|             "integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==", | ||||
|             "version": "2.9.33", | ||||
|             "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.33.tgz", | ||||
|             "integrity": "sha512-vB6ZFx1cA91aiCoVpreLQwCQHS/Cj+9YtjBTwFlTjKXyY0douXV2KV4+fluxdI+grDZ6hTCQeg2HY/aQ9NeLHA==", | ||||
|             "requires": { | ||||
|                 "moment": "^2.10.2" | ||||
|             } | ||||
| @ -9797,9 +9833,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@types/codemirror": { | ||||
|             "version": "5.60.0", | ||||
|             "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.0.tgz", | ||||
|             "integrity": "sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==", | ||||
|             "version": "5.60.1", | ||||
|             "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.1.tgz", | ||||
|             "integrity": "sha512-yV14LQ5VvghnW0uSuCw2bEfZC6NvxHQEckl2w3dEk5l0yPGzQh14dCaWvG5KD/2l3cgFSifR+6nIUD7LDLdUTg==", | ||||
|             "requires": { | ||||
|                 "@types/tern": "*" | ||||
|             } | ||||
| @ -9925,12 +9961,12 @@ | ||||
|             "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==" | ||||
|         }, | ||||
|         "@typescript-eslint/eslint-plugin": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz", | ||||
|             "integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz", | ||||
|             "integrity": "sha512-9yfcNpDaNGQ6/LQOX/KhUFTR1sCKH+PBr234k6hI9XJ0VP5UqGxap0AnNwBnWFk1MNyWBylJH9ZkzBXC+5akZQ==", | ||||
|             "requires": { | ||||
|                 "@typescript-eslint/experimental-utils": "4.28.0", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.0", | ||||
|                 "@typescript-eslint/experimental-utils": "4.28.1", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.1", | ||||
|                 "debug": "^4.3.1", | ||||
|                 "functional-red-black-tree": "^1.0.1", | ||||
|                 "regexpp": "^3.1.0", | ||||
| @ -9939,14 +9975,14 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@typescript-eslint/experimental-utils": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz", | ||||
|             "integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.1.tgz", | ||||
|             "integrity": "sha512-n8/ggadrZ+uyrfrSEchx3jgODdmcx7MzVM2sI3cTpI/YlfSm0+9HEUaWw3aQn2urL2KYlWYMDgn45iLfjDYB+Q==", | ||||
|             "requires": { | ||||
|                 "@types/json-schema": "^7.0.7", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.0", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.1", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.1", | ||||
|                 "eslint-scope": "^5.1.1", | ||||
|                 "eslint-utils": "^3.0.0" | ||||
|             }, | ||||
| @ -9962,37 +9998,37 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@typescript-eslint/parser": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz", | ||||
|             "integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.1.tgz", | ||||
|             "integrity": "sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==", | ||||
|             "requires": { | ||||
|                 "@typescript-eslint/scope-manager": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.0", | ||||
|                 "@typescript-eslint/scope-manager": "4.28.1", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/typescript-estree": "4.28.1", | ||||
|                 "debug": "^4.3.1" | ||||
|             } | ||||
|         }, | ||||
|         "@typescript-eslint/scope-manager": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz", | ||||
|             "integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz", | ||||
|             "integrity": "sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==", | ||||
|             "requires": { | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.0" | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.1" | ||||
|             } | ||||
|         }, | ||||
|         "@typescript-eslint/types": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz", | ||||
|             "integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==" | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.1.tgz", | ||||
|             "integrity": "sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg==" | ||||
|         }, | ||||
|         "@typescript-eslint/typescript-estree": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz", | ||||
|             "integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz", | ||||
|             "integrity": "sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==", | ||||
|             "requires": { | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "@typescript-eslint/visitor-keys": "4.28.1", | ||||
|                 "debug": "^4.3.1", | ||||
|                 "globby": "^11.0.3", | ||||
|                 "is-glob": "^4.0.1", | ||||
| @ -10016,11 +10052,11 @@ | ||||
|             } | ||||
|         }, | ||||
|         "@typescript-eslint/visitor-keys": { | ||||
|             "version": "4.28.0", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz", | ||||
|             "integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==", | ||||
|             "version": "4.28.1", | ||||
|             "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz", | ||||
|             "integrity": "sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==", | ||||
|             "requires": { | ||||
|                 "@typescript-eslint/types": "4.28.0", | ||||
|                 "@typescript-eslint/types": "4.28.1", | ||||
|                 "eslint-visitor-keys": "^2.0.0" | ||||
|             } | ||||
|         }, | ||||
| @ -10170,7 +10206,8 @@ | ||||
|                 "typescript": { | ||||
|                     "version": "3.9.9", | ||||
|                     "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", | ||||
|                     "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==" | ||||
|                     "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", | ||||
|                     "dev": true | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
| @ -10461,9 +10498,9 @@ | ||||
|             "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" | ||||
|         }, | ||||
|         "chart.js": { | ||||
|             "version": "3.3.2", | ||||
|             "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz", | ||||
|             "integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA==" | ||||
|             "version": "3.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz", | ||||
|             "integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g==" | ||||
|         }, | ||||
|         "chartjs-adapter-moment": { | ||||
|             "version": "1.0.0", | ||||
| @ -10899,12 +10936,13 @@ | ||||
|             "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" | ||||
|         }, | ||||
|         "eslint": { | ||||
|             "version": "7.29.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz", | ||||
|             "integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==", | ||||
|             "version": "7.30.0", | ||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", | ||||
|             "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", | ||||
|             "requires": { | ||||
|                 "@babel/code-frame": "7.12.11", | ||||
|                 "@eslint/eslintrc": "^0.4.2", | ||||
|                 "@humanwhocodes/config-array": "^0.5.0", | ||||
|                 "ajv": "^6.10.0", | ||||
|                 "chalk": "^4.0.0", | ||||
|                 "cross-spawn": "^7.0.2", | ||||
| @ -13200,9 +13238,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "rollup": { | ||||
|             "version": "2.52.2", | ||||
|             "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz", | ||||
|             "integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==", | ||||
|             "version": "2.52.7", | ||||
|             "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.7.tgz", | ||||
|             "integrity": "sha512-55cSH4CCU6MaPr9TAOyrIC+7qFCHscL7tkNsm1MBfIJRRqRbCEY0mmeFn4Wg8FKsHtEH8r389Fz38r/o+kgXLg==", | ||||
|             "requires": { | ||||
|                 "fsevents": "~2.3.2" | ||||
|             } | ||||
| @ -13896,9 +13934,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "typescript": { | ||||
|             "version": "4.3.4", | ||||
|             "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", | ||||
|             "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==" | ||||
|             "version": "4.3.5", | ||||
|             "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", | ||||
|             "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==" | ||||
|         }, | ||||
|         "uglify-js": { | ||||
|             "version": "3.13.0", | ||||
|  | ||||
| @ -47,28 +47,28 @@ | ||||
|         "@lingui/cli": "^3.10.2", | ||||
|         "@lingui/core": "^3.10.4", | ||||
|         "@lingui/macro": "^3.10.2", | ||||
|         "@patternfly/patternfly": "^4.108.2", | ||||
|         "@patternfly/patternfly": "^4.115.2", | ||||
|         "@polymer/iron-form": "^3.0.1", | ||||
|         "@polymer/paper-input": "^3.2.1", | ||||
|         "@rollup/plugin-babel": "^5.3.0", | ||||
|         "@rollup/plugin-replace": "^2.4.2", | ||||
|         "@rollup/plugin-typescript": "^8.2.1", | ||||
|         "@sentry/browser": "^6.7.2", | ||||
|         "@sentry/tracing": "^6.7.2", | ||||
|         "@types/chart.js": "^2.9.32", | ||||
|         "@types/codemirror": "5.60.0", | ||||
|         "@sentry/browser": "^6.8.0", | ||||
|         "@sentry/tracing": "^6.8.0", | ||||
|         "@types/chart.js": "^2.9.33", | ||||
|         "@types/codemirror": "5.60.1", | ||||
|         "@types/grecaptcha": "^3.0.2", | ||||
|         "@typescript-eslint/eslint-plugin": "^4.28.0", | ||||
|         "@typescript-eslint/parser": "^4.28.0", | ||||
|         "@typescript-eslint/eslint-plugin": "^4.28.1", | ||||
|         "@typescript-eslint/parser": "^4.28.1", | ||||
|         "@webcomponents/webcomponentsjs": "^2.5.0", | ||||
|         "authentik-api": "file:api", | ||||
|         "babel-plugin-macros": "^3.1.0", | ||||
|         "base64-js": "^1.5.1", | ||||
|         "chart.js": "^3.3.2", | ||||
|         "chart.js": "^3.4.1", | ||||
|         "chartjs-adapter-moment": "^1.0.0", | ||||
|         "codemirror": "^5.62.0", | ||||
|         "construct-style-sheets-polyfill": "^2.4.16", | ||||
|         "eslint": "^7.29.0", | ||||
|         "eslint": "^7.30.0", | ||||
|         "eslint-config-google": "^0.14.0", | ||||
|         "eslint-plugin-custom-elements": "0.0.2", | ||||
|         "eslint-plugin-lit": "^1.5.1", | ||||
| @ -77,7 +77,7 @@ | ||||
|         "lit-html": "^1.4.1", | ||||
|         "moment": "^2.29.1", | ||||
|         "rapidoc": "^9.0.0", | ||||
|         "rollup": "^2.52.2", | ||||
|         "rollup": "^2.52.7", | ||||
|         "rollup-plugin-commonjs": "^10.1.0", | ||||
|         "rollup-plugin-copy": "^3.4.0", | ||||
|         "rollup-plugin-cssimport": "^1.0.2", | ||||
| @ -87,7 +87,7 @@ | ||||
|         "rollup-plugin-terser": "^7.0.2", | ||||
|         "ts-lit-plugin": "^1.2.1", | ||||
|         "tslib": "^2.3.0", | ||||
|         "typescript": "^4.3.4", | ||||
|         "typescript": "^4.3.5", | ||||
|         "webcomponent-qr-code": "^1.0.5", | ||||
|         "yaml": "^1.10.2" | ||||
|     }, | ||||
|  | ||||
| @ -7,7 +7,16 @@ export class LoggingMiddleware implements Middleware { | ||||
|  | ||||
|     post(context: ResponseContext): Promise<Response | void> { | ||||
|         tenant().then(tenant => { | ||||
|             console.debug(`authentik/api[${tenant.matchedDomain}]: ${context.response.status} ${context.init.method} ${context.url}`); | ||||
|             let msg = `authentik/api[${tenant.matchedDomain}]: `; | ||||
|             msg += `${context.response.status} ${context.init.method} ${context.url}`; | ||||
|             if (context.response.status >= 400) { | ||||
|                 context.response.text().then(t => { | ||||
|                     msg += ` => ${t}`; | ||||
|                     console.debug(msg); | ||||
|                 }); | ||||
|             } else { | ||||
|                 console.debug(msg); | ||||
|             } | ||||
|         }); | ||||
|         return Promise.resolve(context.response); | ||||
|     } | ||||
|  | ||||
| @ -139,6 +139,7 @@ body { | ||||
|     /* Card */ | ||||
|     .pf-c-card { | ||||
|         --pf-c-card--BackgroundColor: var(--ak-dark-background-light); | ||||
|         color: var(--ak-dark-foreground); | ||||
|     } | ||||
|     .pf-c-card__title, | ||||
|     .pf-c-card__body { | ||||
|  | ||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | ||||
| export const ERROR_CLASS = "pf-m-danger"; | ||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||
| export const CURRENT_CLASS = "pf-m-current"; | ||||
| export const VERSION = "2021.6.2"; | ||||
| export const VERSION = "2021.6.4"; | ||||
| export const PAGE_SIZE = 20; | ||||
| export const EVENT_REFRESH = "ak-refresh"; | ||||
| export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle"; | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	