Merge branch 'master' into go-proxy
This commit is contained in:
		
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							| @ -59,3 +59,4 @@ pylint-django = "*" | |||||||
| pytest = "*" | pytest = "*" | ||||||
| pytest-django = "*" | pytest-django = "*" | ||||||
| selenium = "*" | selenium = "*" | ||||||
|  | requests-mock = "*" | ||||||
|  | |||||||
							
								
								
									
										176
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										176
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "24f00363590649f2442c6ac28dfe8692f0f317e0a5b91c0696b84610cef299d2" |             "sha256": "17be2923cf8d281e430ec1467aea723806ac6f7c58fc6553ede92317e43f4d14" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @ -56,6 +56,7 @@ | |||||||
|                 "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", |                 "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", | ||||||
|                 "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" |                 "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==3.7.4.post0" |             "version": "==3.7.4.post0" | ||||||
|         }, |         }, | ||||||
|         "aioredis": { |         "aioredis": { | ||||||
| @ -70,6 +71,7 @@ | |||||||
|                 "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", |                 "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", | ||||||
|                 "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" |                 "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==5.0.6" |             "version": "==5.0.6" | ||||||
|         }, |         }, | ||||||
|         "asgiref": { |         "asgiref": { | ||||||
| @ -77,6 +79,7 @@ | |||||||
|                 "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", |                 "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", | ||||||
|                 "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" |                 "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==3.3.4" |             "version": "==3.3.4" | ||||||
|         }, |         }, | ||||||
|         "async-timeout": { |         "async-timeout": { | ||||||
| @ -84,6 +87,7 @@ | |||||||
|                 "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", |                 "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", | ||||||
|                 "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" |                 "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_full_version >= '3.5.3'", | ||||||
|             "version": "==3.0.1" |             "version": "==3.0.1" | ||||||
|         }, |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
| @ -91,6 +95,7 @@ | |||||||
|                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", |                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", | ||||||
|                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" |                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==20.3.0" |             "version": "==20.3.0" | ||||||
|         }, |         }, | ||||||
|         "autobahn": { |         "autobahn": { | ||||||
| @ -98,6 +103,7 @@ | |||||||
|                 "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac", |                 "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac", | ||||||
|                 "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03" |                 "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==21.3.1" |             "version": "==21.3.1" | ||||||
|         }, |         }, | ||||||
|         "automat": { |         "automat": { | ||||||
| @ -116,24 +122,26 @@ | |||||||
|         }, |         }, | ||||||
|         "boto3": { |         "boto3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:35b099fa55f5db6e99a92855b9f320736121ae985340adfc73bc46fb443809e9", |                 "sha256:d856a71d74351649ca8dd59ad17c8c3e79ea57734ff4a38a97611e1e10b06863", | ||||||
|                 "sha256:53fd4c7df86f78e51168f832b42ca1c284333b3f5af0266bf10d13af41aeff5c" |                 "sha256:da1b2c884dbf56cc3ece07940a7b654f41a93b9fc40ee1ed21a76da25a05989c" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.17.61" |             "version": "==1.17.62" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:c765ddd0648e32b375ced8b82bfcc3f8437107278b2d2c73b7da7f41297b5388", |                 "sha256:e4f8cb923edf035c2ae5f6169c70e77e31df70b88919b92b826a6b9bd14511b1", | ||||||
|                 "sha256:d48f94573c75a6c1d6d0152b9e21432083a1b0a0fc39b41f57128464982cb0a0" |                 "sha256:f7c2c5c5ed5212b2628d8fb1c587b31c6e8d413ecbbd1a1cdf6f96ed6f5c8d5e" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.20.61" |             "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.62" | ||||||
|         }, |         }, | ||||||
|         "cachetools": { |         "cachetools": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", |                 "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", | ||||||
|                 "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" |                 "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version ~= '3.5'", | ||||||
|             "version": "==4.2.2" |             "version": "==4.2.2" | ||||||
|         }, |         }, | ||||||
|         "cbor2": { |         "cbor2": { | ||||||
| @ -220,6 +228,7 @@ | |||||||
|                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", |                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", | ||||||
|                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" |                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==4.0.0" |             "version": "==4.0.0" | ||||||
|         }, |         }, | ||||||
|         "click": { |         "click": { | ||||||
| @ -227,6 +236,7 @@ | |||||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", |                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" |                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==7.1.2" |             "version": "==7.1.2" | ||||||
|         }, |         }, | ||||||
|         "click-didyoumean": { |         "click-didyoumean": { | ||||||
| @ -300,6 +310,7 @@ | |||||||
|                 "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", |                 "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", | ||||||
|                 "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" |                 "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==3.0.2" |             "version": "==3.0.2" | ||||||
|         }, |         }, | ||||||
|         "defusedxml": { |         "defusedxml": { | ||||||
| @ -425,6 +436,7 @@ | |||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" |                 "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.18.2" |             "version": "==0.18.2" | ||||||
|         }, |         }, | ||||||
|         "geoip2": { |         "geoip2": { | ||||||
| @ -440,6 +452,7 @@ | |||||||
|                 "sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f", |                 "sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f", | ||||||
|                 "sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206" |                 "sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", | ||||||
|             "version": "==1.30.0" |             "version": "==1.30.0" | ||||||
|         }, |         }, | ||||||
|         "gunicorn": { |         "gunicorn": { | ||||||
| @ -455,6 +468,7 @@ | |||||||
|                 "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", |                 "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", | ||||||
|                 "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" |                 "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==0.12.0" |             "version": "==0.12.0" | ||||||
|         }, |         }, | ||||||
|         "hiredis": { |         "hiredis": { | ||||||
| @ -501,6 +515,7 @@ | |||||||
|                 "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", |                 "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", | ||||||
|                 "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" |                 "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==2.0.0" |             "version": "==2.0.0" | ||||||
|         }, |         }, | ||||||
|         "httptools": { |         "httptools": { | ||||||
| @ -549,6 +564,7 @@ | |||||||
|                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", |                 "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", | ||||||
|                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" |                 "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==0.5.1" |             "version": "==0.5.1" | ||||||
|         }, |         }, | ||||||
|         "itypes": { |         "itypes": { | ||||||
| @ -563,6 +579,7 @@ | |||||||
|                 "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", |                 "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", | ||||||
|                 "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" |                 "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==2.11.3" |             "version": "==2.11.3" | ||||||
|         }, |         }, | ||||||
|         "jmespath": { |         "jmespath": { | ||||||
| @ -570,6 +587,7 @@ | |||||||
|                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", |                 "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", | ||||||
|                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" |                 "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.10.0" |             "version": "==0.10.0" | ||||||
|         }, |         }, | ||||||
|         "jsonschema": { |         "jsonschema": { | ||||||
| @ -584,6 +602,7 @@ | |||||||
|                 "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", |                 "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", | ||||||
|                 "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" |                 "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==5.0.2" |             "version": "==5.0.2" | ||||||
|         }, |         }, | ||||||
|         "kubernetes": { |         "kubernetes": { | ||||||
| @ -596,8 +615,11 @@ | |||||||
|         }, |         }, | ||||||
|         "ldap3": { |         "ldap3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  |                 "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056", | ||||||
|                 "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", |                 "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", | ||||||
|                 "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" |                 "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57", | ||||||
|  |                 "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c", | ||||||
|  |                 "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.9" |             "version": "==2.9" | ||||||
| @ -607,18 +629,24 @@ | |||||||
|                 "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", |                 "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", | ||||||
|                 "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", |                 "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", | ||||||
|                 "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", |                 "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", | ||||||
|  |                 "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae", | ||||||
|                 "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", |                 "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", | ||||||
|                 "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", |                 "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", | ||||||
|                 "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", |                 "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", | ||||||
|                 "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", |                 "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", | ||||||
|  |                 "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59", | ||||||
|                 "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", |                 "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", | ||||||
|                 "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", |                 "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", | ||||||
|  |                 "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96", | ||||||
|                 "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", |                 "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", | ||||||
|                 "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", |                 "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", | ||||||
|                 "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", |                 "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", | ||||||
|  |                 "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354", | ||||||
|                 "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", |                 "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", | ||||||
|                 "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", |                 "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", | ||||||
|  |                 "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16", | ||||||
|                 "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", |                 "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", | ||||||
|  |                 "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a", | ||||||
|                 "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", |                 "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", | ||||||
|                 "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", |                 "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", | ||||||
|                 "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", |                 "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", | ||||||
| @ -631,10 +659,14 @@ | |||||||
|                 "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", |                 "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", | ||||||
|                 "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", |                 "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", | ||||||
|                 "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", |                 "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", | ||||||
|  |                 "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617", | ||||||
|                 "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", |                 "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", | ||||||
|  |                 "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92", | ||||||
|                 "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", |                 "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", | ||||||
|                 "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", |                 "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", | ||||||
|  |                 "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24", | ||||||
|                 "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", |                 "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", | ||||||
|  |                 "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e", | ||||||
|                 "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", |                 "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", | ||||||
|                 "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", |                 "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", | ||||||
|                 "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", |                 "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", | ||||||
| @ -699,12 +731,14 @@ | |||||||
|                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", |                 "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", | ||||||
|                 "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" |                 "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.1.1" |             "version": "==1.1.1" | ||||||
|         }, |         }, | ||||||
|         "maxminddb": { |         "maxminddb": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" |                 "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==2.0.3" |             "version": "==2.0.3" | ||||||
|         }, |         }, | ||||||
|         "msgpack": { |         "msgpack": { | ||||||
| @ -780,6 +814,7 @@ | |||||||
|                 "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", |                 "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", | ||||||
|                 "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" |                 "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==5.1.0" |             "version": "==5.1.0" | ||||||
|         }, |         }, | ||||||
|         "oauthlib": { |         "oauthlib": { | ||||||
| @ -787,6 +822,7 @@ | |||||||
|                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", |                 "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", | ||||||
|                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" |                 "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.1.0" |             "version": "==3.1.0" | ||||||
|         }, |         }, | ||||||
|         "packaging": { |         "packaging": { | ||||||
| @ -802,6 +838,7 @@ | |||||||
|                 "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa", |                 "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa", | ||||||
|                 "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d" |                 "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.10.1" |             "version": "==0.10.1" | ||||||
|         }, |         }, | ||||||
|         "prompt-toolkit": { |         "prompt-toolkit": { | ||||||
| @ -809,6 +846,7 @@ | |||||||
|                 "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", |                 "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", | ||||||
|                 "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" |                 "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_full_version >= '3.6.1'", | ||||||
|             "version": "==3.0.18" |             "version": "==3.0.18" | ||||||
|         }, |         }, | ||||||
|         "psycopg2-binary": { |         "psycopg2-binary": { | ||||||
| @ -854,15 +892,37 @@ | |||||||
|         }, |         }, | ||||||
|         "pyasn1": { |         "pyasn1": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  |                 "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", | ||||||
|  |                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", | ||||||
|  |                 "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", | ||||||
|  |                 "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", | ||||||
|                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", |                 "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", | ||||||
|                 "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" |                 "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", | ||||||
|  |                 "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", | ||||||
|  |                 "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", | ||||||
|  |                 "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", | ||||||
|  |                 "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", | ||||||
|  |                 "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", | ||||||
|  |                 "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3", | ||||||
|  |                 "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.4.8" |             "version": "==0.4.8" | ||||||
|         }, |         }, | ||||||
|         "pyasn1-modules": { |         "pyasn1-modules": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  |                 "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", | ||||||
|  |                 "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", | ||||||
|  |                 "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", | ||||||
|  |                 "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405", | ||||||
|  |                 "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", | ||||||
|  |                 "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", | ||||||
|                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", |                 "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", | ||||||
|                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" |                 "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", | ||||||
|  |                 "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", | ||||||
|  |                 "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", | ||||||
|  |                 "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", | ||||||
|  |                 "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", | ||||||
|  |                 "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.2.8" |             "version": "==0.2.8" | ||||||
|         }, |         }, | ||||||
| @ -871,6 +931,7 @@ | |||||||
|                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", |                 "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", | ||||||
|                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" |                 "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.20" |             "version": "==2.20" | ||||||
|         }, |         }, | ||||||
|         "pycryptodome": { |         "pycryptodome": { | ||||||
| @ -914,6 +975,7 @@ | |||||||
|                 "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", |                 "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", | ||||||
|                 "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" |                 "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==2.0.2" |             "version": "==2.0.2" | ||||||
|         }, |         }, | ||||||
|         "pyjwt": { |         "pyjwt": { | ||||||
| @ -936,12 +998,14 @@ | |||||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", |                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" |                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.4.7" |             "version": "==2.4.7" | ||||||
|         }, |         }, | ||||||
|         "pyrsistent": { |         "pyrsistent": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" |                 "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==0.17.3" |             "version": "==0.17.3" | ||||||
|         }, |         }, | ||||||
|         "python-dateutil": { |         "python-dateutil": { | ||||||
| @ -949,6 +1013,7 @@ | |||||||
|                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", |                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", | ||||||
|                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" |                 "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.8.1" |             "version": "==2.8.1" | ||||||
|         }, |         }, | ||||||
|         "python-dotenv": { |         "python-dotenv": { | ||||||
| @ -1005,6 +1070,7 @@ | |||||||
|                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", |                 "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", | ||||||
|                 "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" |                 "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==3.5.3" |             "version": "==3.5.3" | ||||||
|         }, |         }, | ||||||
|         "requests": { |         "requests": { | ||||||
| @ -1012,12 +1078,14 @@ | |||||||
|                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", |                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", | ||||||
|                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" |                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==2.25.1" |             "version": "==2.25.1" | ||||||
|         }, |         }, | ||||||
|         "requests-oauthlib": { |         "requests-oauthlib": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", |                 "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", | ||||||
|                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" |                 "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", | ||||||
|  |                 "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.3.0" |             "version": "==1.3.0" | ||||||
| @ -1035,6 +1103,7 @@ | |||||||
|                 "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28", |                 "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28", | ||||||
|                 "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22" |                 "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3'", | ||||||
|             "version": "==0.17.4" |             "version": "==0.17.4" | ||||||
|         }, |         }, | ||||||
|         "ruamel.yaml.clib": { |         "ruamel.yaml.clib": { | ||||||
| @ -1071,7 +1140,7 @@ | |||||||
|                 "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", |                 "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", | ||||||
|                 "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" |                 "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" | ||||||
|             ], |             ], | ||||||
|             "markers": "platform_python_implementation == 'CPython' and python_version < '3.10'", |             "markers": "python_version < '3.10' and platform_python_implementation == 'CPython'", | ||||||
|             "version": "==0.2.2" |             "version": "==0.2.2" | ||||||
|         }, |         }, | ||||||
|         "s3transfer": { |         "s3transfer": { | ||||||
| @ -1102,6 +1171,7 @@ | |||||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", |                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" |                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.15.0" |             "version": "==1.15.0" | ||||||
|         }, |         }, | ||||||
|         "sqlparse": { |         "sqlparse": { | ||||||
| @ -1109,6 +1179,7 @@ | |||||||
|                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", |                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", | ||||||
|                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" |                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==0.4.1" |             "version": "==0.4.1" | ||||||
|         }, |         }, | ||||||
|         "structlog": { |         "structlog": { | ||||||
| @ -1164,21 +1235,23 @@ | |||||||
|                 "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8", |                 "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8", | ||||||
|                 "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb" |                 "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==21.2.1" |             "version": "==21.2.1" | ||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |         "typing-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", |                 "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", | ||||||
|                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", |                 "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", | ||||||
|                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" |                 "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.7.4.3" |             "version": "==3.10.0.0" | ||||||
|         }, |         }, | ||||||
|         "uritemplate": { |         "uritemplate": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", |                 "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", | ||||||
|                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" |                 "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==3.0.1" |             "version": "==3.0.1" | ||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
| @ -1223,6 +1296,7 @@ | |||||||
|                 "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", |                 "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", | ||||||
|                 "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" |                 "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==5.0.0" |             "version": "==5.0.0" | ||||||
|         }, |         }, | ||||||
|         "watchgod": { |         "watchgod": { | ||||||
| @ -1252,6 +1326,7 @@ | |||||||
|                 "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663", |                 "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663", | ||||||
|                 "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f" |                 "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.58.0" |             "version": "==0.58.0" | ||||||
|         }, |         }, | ||||||
|         "websockets": { |         "websockets": { | ||||||
| @ -1338,6 +1413,7 @@ | |||||||
|                 "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", |                 "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", | ||||||
|                 "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" |                 "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==1.6.3" |             "version": "==1.6.3" | ||||||
|         }, |         }, | ||||||
|         "zope.interface": { |         "zope.interface": { | ||||||
| @ -1394,6 +1470,7 @@ | |||||||
|                 "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", |                 "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", | ||||||
|                 "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" |                 "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==5.4.0" |             "version": "==5.4.0" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @ -1410,6 +1487,7 @@ | |||||||
|                 "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", |                 "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", | ||||||
|                 "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" |                 "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version ~= '3.6'", | ||||||
|             "version": "==2.5.6" |             "version": "==2.5.6" | ||||||
|         }, |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
| @ -1417,6 +1495,7 @@ | |||||||
|                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", |                 "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", | ||||||
|                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" |                 "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==20.3.0" |             "version": "==20.3.0" | ||||||
|         }, |         }, | ||||||
|         "bandit": { |         "bandit": { | ||||||
| @ -1442,11 +1521,27 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.0.1" |             "version": "==1.0.1" | ||||||
|         }, |         }, | ||||||
|  |         "certifi": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", | ||||||
|  |                 "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" | ||||||
|  |             ], | ||||||
|  |             "version": "==2020.12.5" | ||||||
|  |         }, | ||||||
|  |         "chardet": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", | ||||||
|  |                 "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|  |             "version": "==4.0.0" | ||||||
|  |         }, | ||||||
|         "click": { |         "click": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", |                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" |                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|             "version": "==7.1.2" |             "version": "==7.1.2" | ||||||
|         }, |         }, | ||||||
|         "colorama": { |         "colorama": { | ||||||
| @ -1520,14 +1615,23 @@ | |||||||
|                 "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", |                 "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", | ||||||
|                 "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" |                 "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.4'", | ||||||
|             "version": "==4.0.7" |             "version": "==4.0.7" | ||||||
|         }, |         }, | ||||||
|         "gitpython": { |         "gitpython": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e", |                 "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b", | ||||||
|                 "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867" |                 "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.1.15" |             "markers": "python_version >= '3.4'", | ||||||
|  |             "version": "==3.1.14" | ||||||
|  |         }, | ||||||
|  |         "idna": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||||
|  |                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||||
|  |             ], | ||||||
|  |             "version": "==2.10" | ||||||
|         }, |         }, | ||||||
|         "iniconfig": { |         "iniconfig": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1541,6 +1645,7 @@ | |||||||
|                 "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", |                 "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", | ||||||
|                 "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" |                 "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6' and python_version < '4.0'", | ||||||
|             "version": "==5.8.0" |             "version": "==5.8.0" | ||||||
|         }, |         }, | ||||||
|         "lazy-object-proxy": { |         "lazy-object-proxy": { | ||||||
| @ -1568,6 +1673,7 @@ | |||||||
|                 "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", |                 "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", | ||||||
|                 "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" |                 "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", | ||||||
|             "version": "==1.6.0" |             "version": "==1.6.0" | ||||||
|         }, |         }, | ||||||
|         "mccabe": { |         "mccabe": { | ||||||
| @ -1604,6 +1710,7 @@ | |||||||
|                 "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", |                 "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", | ||||||
|                 "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" |                 "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6'", | ||||||
|             "version": "==5.6.0" |             "version": "==5.6.0" | ||||||
|         }, |         }, | ||||||
|         "pluggy": { |         "pluggy": { | ||||||
| @ -1611,6 +1718,7 @@ | |||||||
|                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", |                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", | ||||||
|                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" |                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.13.1" |             "version": "==0.13.1" | ||||||
|         }, |         }, | ||||||
|         "py": { |         "py": { | ||||||
| @ -1618,6 +1726,7 @@ | |||||||
|                 "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", |                 "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", | ||||||
|                 "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" |                 "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.10.0" |             "version": "==1.10.0" | ||||||
|         }, |         }, | ||||||
|         "pylint": { |         "pylint": { | ||||||
| @ -1648,6 +1757,7 @@ | |||||||
|                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", |                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", | ||||||
|                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" |                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==2.4.7" |             "version": "==2.4.7" | ||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
| @ -1747,6 +1857,22 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==2021.4.4" |             "version": "==2021.4.4" | ||||||
|         }, |         }, | ||||||
|  |         "requests": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", | ||||||
|  |                 "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", | ||||||
|  |             "version": "==2.25.1" | ||||||
|  |         }, | ||||||
|  |         "requests-mock": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:33296f228d8c5df11a7988b741325422480baddfdf5dd9318fd0eb40c3ed8595", | ||||||
|  |                 "sha256:5c8ef0254c14a84744be146e9799dc13ebc4f6186058112d9aeed96b131b58e2" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==1.9.2" | ||||||
|  |         }, | ||||||
|         "selenium": { |         "selenium": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", |                 "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", | ||||||
| @ -1760,6 +1886,7 @@ | |||||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", |                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" |                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==1.15.0" |             "version": "==1.15.0" | ||||||
|         }, |         }, | ||||||
|         "smmap": { |         "smmap": { | ||||||
| @ -1767,6 +1894,7 @@ | |||||||
|                 "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", |                 "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", | ||||||
|                 "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" |                 "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.5'", | ||||||
|             "version": "==4.0.0" |             "version": "==4.0.0" | ||||||
|         }, |         }, | ||||||
|         "stevedore": { |         "stevedore": { | ||||||
| @ -1774,6 +1902,7 @@ | |||||||
|                 "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", |                 "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", | ||||||
|                 "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" |                 "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==3.3.0" |             "version": "==3.3.0" | ||||||
|         }, |         }, | ||||||
|         "toml": { |         "toml": { | ||||||
| @ -1781,6 +1910,7 @@ | |||||||
|                 "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", |                 "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", | ||||||
|                 "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" |                 "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" | ||||||
|             ], |             ], | ||||||
|  |             "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", | ||||||
|             "version": "==0.10.2" |             "version": "==0.10.2" | ||||||
|         }, |         }, | ||||||
|         "typed-ast": { |         "typed-ast": { | ||||||
| @ -1820,11 +1950,11 @@ | |||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |         "typing-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", |                 "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", | ||||||
|                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", |                 "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", | ||||||
|                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" |                 "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.7.4.3" |             "version": "==3.10.0.0" | ||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
|             "extras": [ |             "extras": [ | ||||||
|  | |||||||
| @ -63,6 +63,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet | |||||||
| from authentik.sources.oauth.api.source_connection import ( | from authentik.sources.oauth.api.source_connection import ( | ||||||
|     UserOAuthSourceConnectionViewSet, |     UserOAuthSourceConnectionViewSet, | ||||||
| ) | ) | ||||||
|  | from authentik.sources.plex.api import PlexSourceViewSet | ||||||
| from authentik.sources.saml.api import SAMLSourceViewSet | from authentik.sources.saml.api import SAMLSourceViewSet | ||||||
| from authentik.stages.authenticator_static.api import ( | from authentik.stages.authenticator_static.api import ( | ||||||
|     AuthenticatorStaticStageViewSet, |     AuthenticatorStaticStageViewSet, | ||||||
| @ -136,6 +137,7 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS | |||||||
| router.register("sources/ldap", LDAPSourceViewSet) | router.register("sources/ldap", LDAPSourceViewSet) | ||||||
| router.register("sources/saml", SAMLSourceViewSet) | router.register("sources/saml", SAMLSourceViewSet) | ||||||
| router.register("sources/oauth", OAuthSourceViewSet) | router.register("sources/oauth", OAuthSourceViewSet) | ||||||
|  | router.register("sources/plex", PlexSourceViewSet) | ||||||
|  |  | ||||||
| router.register("policies/all", PolicyViewSet) | router.register("policies/all", PolicyViewSet) | ||||||
| router.register("policies/bindings", PolicyBindingViewSet) | router.register("policies/bindings", PolicyBindingViewSet) | ||||||
|  | |||||||
| @ -45,6 +45,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer): | |||||||
|             "verbose_name", |             "verbose_name", | ||||||
|             "verbose_name_plural", |             "verbose_name_plural", | ||||||
|             "policy_engine_mode", |             "policy_engine_mode", | ||||||
|  |             "user_matching_mode", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								authentik/core/migrations/0020_source_user_matching_mode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								authentik/core/migrations/0020_source_user_matching_mode.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-05-03 17:06 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0019_source_managed"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="source", | ||||||
|  |             name="user_matching_mode", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("identifier", "Use the source-specific identifier"), | ||||||
|  |                     ( | ||||||
|  |                         "email_link", | ||||||
|  |                         "Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "email_deny", | ||||||
|  |                         "Use the user's email address, but deny enrollment when the email address already exists.", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "username_link", | ||||||
|  |                         "Link to a user with identical username address. Can have security implications when a username is used with another source.", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "username_deny", | ||||||
|  |                         "Use the user's username, but deny enrollment when the username already exists.", | ||||||
|  |                     ), | ||||||
|  |                 ], | ||||||
|  |                 default="identifier", | ||||||
|  |                 help_text="How the source determines if an existing user should be authenticated or a new user enrolled.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -240,6 +240,30 @@ class Application(PolicyBindingModel): | |||||||
|         verbose_name_plural = _("Applications") |         verbose_name_plural = _("Applications") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SourceUserMatchingModes(models.TextChoices): | ||||||
|  |     """Different modes a source can handle new/returning users""" | ||||||
|  |  | ||||||
|  |     IDENTIFIER = "identifier", _("Use the source-specific identifier") | ||||||
|  |     EMAIL_LINK = "email_link", _( | ||||||
|  |         ( | ||||||
|  |             "Link to a user with identical email address. Can have security implications " | ||||||
|  |             "when a source doesn't validate email addresses." | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |     EMAIL_DENY = "email_deny", _( | ||||||
|  |         "Use the user's email address, but deny enrollment when the email address already exists." | ||||||
|  |     ) | ||||||
|  |     USERNAME_LINK = "username_link", _( | ||||||
|  |         ( | ||||||
|  |             "Link to a user with identical username address. Can have security implications " | ||||||
|  |             "when a username is used with another source." | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |     USERNAME_DENY = "username_deny", _( | ||||||
|  |         "Use the user's username, but deny enrollment when the username already exists." | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Source(ManagedModel, SerializerModel, PolicyBindingModel): | class Source(ManagedModel, SerializerModel, PolicyBindingModel): | ||||||
|     """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" |     """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" | ||||||
|  |  | ||||||
| @ -272,6 +296,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | |||||||
|         related_name="source_enrollment", |         related_name="source_enrollment", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     user_matching_mode = models.TextField( | ||||||
|  |         choices=SourceUserMatchingModes.choices, | ||||||
|  |         default=SourceUserMatchingModes.IDENTIFIER, | ||||||
|  |         help_text=_( | ||||||
|  |             ( | ||||||
|  |                 "How the source determines if an existing user should be authenticated or " | ||||||
|  |                 "a new user enrolled." | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     objects = InheritanceManager() |     objects = InheritanceManager() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @ -301,6 +336,8 @@ class UserSourceConnection(CreatedUpdatedModel): | |||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|     source = models.ForeignKey(Source, on_delete=models.CASCADE) |     source = models.ForeignKey(Source, on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|  |     objects = InheritanceManager() | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         unique_together = (("user", "source"),) |         unique_together = (("user", "source"),) | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								authentik/core/sources/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/core/sources/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										261
									
								
								authentik/core/sources/flow_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								authentik/core/sources/flow_manager.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,261 @@ | |||||||
|  | """Source decision helper""" | ||||||
|  | from enum import Enum | ||||||
|  | from typing import Any, Optional, Type | ||||||
|  |  | ||||||
|  | from django.contrib import messages | ||||||
|  | from django.db.models.query_utils import Q | ||||||
|  | from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest | ||||||
|  | from django.shortcuts import redirect | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.translation import gettext as _ | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.core.models import ( | ||||||
|  |     Source, | ||||||
|  |     SourceUserMatchingModes, | ||||||
|  |     User, | ||||||
|  |     UserSourceConnection, | ||||||
|  | ) | ||||||
|  | from authentik.core.sources.stage import ( | ||||||
|  |     PLAN_CONTEXT_SOURCES_CONNECTION, | ||||||
|  |     PostUserEnrollmentStage, | ||||||
|  | ) | ||||||
|  | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.flows.models import Flow, Stage, in_memory_stage | ||||||
|  | from authentik.flows.planner import ( | ||||||
|  |     PLAN_CONTEXT_PENDING_USER, | ||||||
|  |     PLAN_CONTEXT_REDIRECT, | ||||||
|  |     PLAN_CONTEXT_SOURCE, | ||||||
|  |     PLAN_CONTEXT_SSO, | ||||||
|  |     FlowPlanner, | ||||||
|  | ) | ||||||
|  | from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||||
|  | from authentik.lib.utils.urls import redirect_with_qs | ||||||
|  | from authentik.policies.utils import delete_none_keys | ||||||
|  | from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | ||||||
|  | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Action(Enum): | ||||||
|  |     """Actions that can be decided based on the request | ||||||
|  |     and source settings""" | ||||||
|  |  | ||||||
|  |     LINK = "link" | ||||||
|  |     AUTH = "auth" | ||||||
|  |     ENROLL = "enroll" | ||||||
|  |     DENY = "deny" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SourceFlowManager: | ||||||
|  |     """Help sources decide what they should do after authorization. Based on source settings and | ||||||
|  |     previous connections, authenticate the user, enroll a new user, link to an existing user | ||||||
|  |     or deny the request.""" | ||||||
|  |  | ||||||
|  |     source: Source | ||||||
|  |     request: HttpRequest | ||||||
|  |  | ||||||
|  |     identifier: str | ||||||
|  |  | ||||||
|  |     connection_type: Type[UserSourceConnection] = UserSourceConnection | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         source: Source, | ||||||
|  |         request: HttpRequest, | ||||||
|  |         identifier: str, | ||||||
|  |         enroll_info: dict[str, Any], | ||||||
|  |     ) -> None: | ||||||
|  |         self.source = source | ||||||
|  |         self.request = request | ||||||
|  |         self.identifier = identifier | ||||||
|  |         self.enroll_info = enroll_info | ||||||
|  |         self._logger = get_logger().bind(source=source, identifier=identifier) | ||||||
|  |  | ||||||
|  |     # pylint: disable=too-many-return-statements | ||||||
|  |     def get_action(self, **kwargs) -> tuple[Action, Optional[UserSourceConnection]]: | ||||||
|  |         """decide which action should be taken""" | ||||||
|  |         new_connection = self.connection_type( | ||||||
|  |             source=self.source, identifier=self.identifier | ||||||
|  |         ) | ||||||
|  |         # When request is authenticated, always link | ||||||
|  |         if self.request.user.is_authenticated: | ||||||
|  |             new_connection.user = self.request.user | ||||||
|  |             new_connection = self.update_connection(new_connection, **kwargs) | ||||||
|  |             new_connection.save() | ||||||
|  |             return Action.LINK, new_connection | ||||||
|  |  | ||||||
|  |         existing_connections = self.connection_type.objects.filter( | ||||||
|  |             source=self.source, identifier=self.identifier | ||||||
|  |         ) | ||||||
|  |         if existing_connections.exists(): | ||||||
|  |             connection = existing_connections.first() | ||||||
|  |             return Action.AUTH, self.update_connection(connection, **kwargs) | ||||||
|  |         # No connection exists, but we match on identifier, so enroll | ||||||
|  |         if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER: | ||||||
|  |             # We don't save the connection here cause it doesn't have a user assigned yet | ||||||
|  |             return Action.ENROLL, self.update_connection(new_connection, **kwargs) | ||||||
|  |  | ||||||
|  |         # Check for existing users with matching attributes | ||||||
|  |         query = Q() | ||||||
|  |         # Either query existing user based on email or username | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.EMAIL_LINK, | ||||||
|  |             SourceUserMatchingModes.EMAIL_DENY, | ||||||
|  |         ]: | ||||||
|  |             if not self.enroll_info.get("email", None): | ||||||
|  |                 self._logger.warning("Refusing to use none email", source=self.source) | ||||||
|  |                 return Action.DENY, None | ||||||
|  |             query = Q(email__exact=self.enroll_info.get("email", None)) | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.USERNAME_LINK, | ||||||
|  |             SourceUserMatchingModes.USERNAME_DENY, | ||||||
|  |         ]: | ||||||
|  |             if not self.enroll_info.get("username", None): | ||||||
|  |                 self._logger.warning( | ||||||
|  |                     "Refusing to use none username", source=self.source | ||||||
|  |                 ) | ||||||
|  |                 return Action.DENY, None | ||||||
|  |             query = Q(username__exact=self.enroll_info.get("username", None)) | ||||||
|  |         matching_users = User.objects.filter(query) | ||||||
|  |         # No matching users, always enroll | ||||||
|  |         if not matching_users.exists(): | ||||||
|  |             return Action.ENROLL, self.update_connection(new_connection, **kwargs) | ||||||
|  |  | ||||||
|  |         user = matching_users.first() | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.EMAIL_LINK, | ||||||
|  |             SourceUserMatchingModes.USERNAME_LINK, | ||||||
|  |         ]: | ||||||
|  |             new_connection.user = user | ||||||
|  |             new_connection = self.update_connection(new_connection, **kwargs) | ||||||
|  |             new_connection.save() | ||||||
|  |             return Action.LINK, new_connection | ||||||
|  |         if self.source.user_matching_mode in [ | ||||||
|  |             SourceUserMatchingModes.EMAIL_DENY, | ||||||
|  |             SourceUserMatchingModes.USERNAME_DENY, | ||||||
|  |         ]: | ||||||
|  |             return Action.DENY, None | ||||||
|  |         return Action.DENY, None | ||||||
|  |  | ||||||
|  |     def update_connection( | ||||||
|  |         self, connection: UserSourceConnection, **kwargs | ||||||
|  |     ) -> UserSourceConnection: | ||||||
|  |         """Optionally make changes to the connection after it is looked up/created.""" | ||||||
|  |         return connection | ||||||
|  |  | ||||||
|  |     def get_flow(self, **kwargs) -> HttpResponse: | ||||||
|  |         """Get the flow response based on user_matching_mode""" | ||||||
|  |         action, connection = self.get_action() | ||||||
|  |         if action == Action.LINK: | ||||||
|  |             self._logger.debug("Linking existing user") | ||||||
|  |             return self.handle_existing_user_link() | ||||||
|  |         if not connection: | ||||||
|  |             return redirect("/") | ||||||
|  |         if action == Action.AUTH: | ||||||
|  |             self._logger.debug("Handling auth user") | ||||||
|  |             return self.handle_auth_user(connection) | ||||||
|  |         if action == Action.ENROLL: | ||||||
|  |             self._logger.debug("Handling enrollment of new user") | ||||||
|  |             return self.handle_enroll(connection) | ||||||
|  |         return redirect("/") | ||||||
|  |  | ||||||
|  |     # pylint: disable=unused-argument | ||||||
|  |     def get_stages_to_append(self, flow: Flow) -> list[Stage]: | ||||||
|  |         """Hook to override stages which are appended to the flow""" | ||||||
|  |         if flow.slug == self.source.enrollment_flow.slug: | ||||||
|  |             return [ | ||||||
|  |                 in_memory_stage(PostUserEnrollmentStage), | ||||||
|  |             ] | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |     def _handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse: | ||||||
|  |         """Prepare Authentication Plan, redirect user FlowExecutor""" | ||||||
|  |         # Ensure redirect is carried through when user was trying to | ||||||
|  |         # authorize application | ||||||
|  |         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||||
|  |             NEXT_ARG_NAME, "authentik_core:if-admin" | ||||||
|  |         ) | ||||||
|  |         kwargs.update( | ||||||
|  |             { | ||||||
|  |                 # Since we authenticate the user by their token, they have no backend set | ||||||
|  |                 PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend", | ||||||
|  |                 PLAN_CONTEXT_SSO: True, | ||||||
|  |                 PLAN_CONTEXT_SOURCE: self.source, | ||||||
|  |                 PLAN_CONTEXT_REDIRECT: final_redirect, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         if not flow: | ||||||
|  |             return HttpResponseBadRequest() | ||||||
|  |         # We run the Flow planner here so we can pass the Pending user in the context | ||||||
|  |         planner = FlowPlanner(flow) | ||||||
|  |         plan = planner.plan(self.request, kwargs) | ||||||
|  |         for stage in self.get_stages_to_append(flow): | ||||||
|  |             plan.append(stage) | ||||||
|  |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|  |         return redirect_with_qs( | ||||||
|  |             "authentik_core:if-flow", | ||||||
|  |             self.request.GET, | ||||||
|  |             flow_slug=flow.slug, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     # pylint: disable=unused-argument | ||||||
|  |     def handle_auth_user( | ||||||
|  |         self, | ||||||
|  |         connection: UserSourceConnection, | ||||||
|  |     ) -> HttpResponse: | ||||||
|  |         """Login user and redirect.""" | ||||||
|  |         messages.success( | ||||||
|  |             self.request, | ||||||
|  |             _( | ||||||
|  |                 "Successfully authenticated with %(source)s!" | ||||||
|  |                 % {"source": self.source.name} | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user} | ||||||
|  |         return self._handle_login_flow(self.source.authentication_flow, **flow_kwargs) | ||||||
|  |  | ||||||
|  |     def handle_existing_user_link( | ||||||
|  |         self, | ||||||
|  |     ) -> HttpResponse: | ||||||
|  |         """Handler when the user was already authenticated and linked an external source | ||||||
|  |         to their account.""" | ||||||
|  |         Event.new( | ||||||
|  |             EventAction.SOURCE_LINKED, | ||||||
|  |             message="Linked Source", | ||||||
|  |             source=self.source, | ||||||
|  |         ).from_http(self.request) | ||||||
|  |         messages.success( | ||||||
|  |             self.request, | ||||||
|  |             _("Successfully linked %(source)s!" % {"source": self.source.name}), | ||||||
|  |         ) | ||||||
|  |         return redirect( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_core:if-admin", | ||||||
|  |             ) | ||||||
|  |             + f"#/user;page-{self.source.slug}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def handle_enroll( | ||||||
|  |         self, | ||||||
|  |         connection: UserSourceConnection, | ||||||
|  |     ) -> HttpResponse: | ||||||
|  |         """User was not authenticated and previous request was not authenticated.""" | ||||||
|  |         messages.success( | ||||||
|  |             self.request, | ||||||
|  |             _( | ||||||
|  |                 "Successfully authenticated with %(source)s!" | ||||||
|  |                 % {"source": self.source.name} | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # We run the Flow planner here so we can pass the Pending user in the context | ||||||
|  |         if not self.source.enrollment_flow: | ||||||
|  |             self._logger.warning("source has no enrollment flow") | ||||||
|  |             return HttpResponseBadRequest() | ||||||
|  |         return self._handle_login_flow( | ||||||
|  |             self.source.enrollment_flow, | ||||||
|  |             **{ | ||||||
|  |                 PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info), | ||||||
|  |                 PLAN_CONTEXT_SOURCES_CONNECTION: connection, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
| @ -1,32 +1,30 @@ | |||||||
| """OAuth Stages""" | """Source flow manager stages""" | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| 
 | 
 | ||||||
| from authentik.core.models import User | from authentik.core.models import User, UserSourceConnection | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER | ||||||
| from authentik.flows.stage import StageView | from authentik.flows.stage import StageView | ||||||
| from authentik.sources.oauth.models import UserOAuthSourceConnection |  | ||||||
| 
 | 
 | ||||||
| PLAN_CONTEXT_SOURCES_OAUTH_ACCESS = "sources_oauth_access" | PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PostUserEnrollmentStage(StageView): | class PostUserEnrollmentStage(StageView): | ||||||
|     """Dynamically injected stage which saves the OAuth Connection after |     """Dynamically injected stage which saves the Connection after | ||||||
|     the user has been enrolled.""" |     the user has been enrolled.""" | ||||||
| 
 | 
 | ||||||
|     # pylint: disable=unused-argument |     # pylint: disable=unused-argument | ||||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: |     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|         """Stage used after the user has been enrolled""" |         """Stage used after the user has been enrolled""" | ||||||
|         access: UserOAuthSourceConnection = self.executor.plan.context[ |         connection: UserSourceConnection = self.executor.plan.context[ | ||||||
|             PLAN_CONTEXT_SOURCES_OAUTH_ACCESS |             PLAN_CONTEXT_SOURCES_CONNECTION | ||||||
|         ] |         ] | ||||||
|         user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] |         user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] | ||||||
|         access.user = user |         connection.user = user | ||||||
|         access.save() |         connection.save() | ||||||
|         UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user) |  | ||||||
|         Event.new( |         Event.new( | ||||||
|             EventAction.SOURCE_LINKED, |             EventAction.SOURCE_LINKED, | ||||||
|             message="Linked OAuth Source", |             message="Linked Source", | ||||||
|             source=access.source, |             source=connection.source, | ||||||
|         ).from_http(self.request) |         ).from_http(self.request) | ||||||
|         return self.executor.stage_ok() |         return self.executor.stage_ok() | ||||||
| @ -1,11 +1,14 @@ | |||||||
| """authentik core models tests""" | """authentik core models tests""" | ||||||
| from time import sleep | from time import sleep | ||||||
|  | from typing import Callable, Type | ||||||
|  |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils.timezone import now | from django.utils.timezone import now | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
|  |  | ||||||
| from authentik.core.models import Token | from authentik.core.models import Provider, Source, Token | ||||||
|  | from authentik.flows.models import Stage | ||||||
|  | from authentik.lib.utils.reflection import all_subclasses | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestModels(TestCase): | class TestModels(TestCase): | ||||||
| @ -24,3 +27,40 @@ class TestModels(TestCase): | |||||||
|         ) |         ) | ||||||
|         sleep(0.5) |         sleep(0.5) | ||||||
|         self.assertFalse(token.is_expired) |         self.assertFalse(token.is_expired) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def source_tester_factory(test_model: Type[Stage]) -> Callable: | ||||||
|  |     """Test source""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         model_class = None | ||||||
|  |         if test_model._meta.abstract: | ||||||
|  |             model_class = test_model.__bases__[0]() | ||||||
|  |         else: | ||||||
|  |             model_class = test_model() | ||||||
|  |         model_class.slug = "test" | ||||||
|  |         self.assertIsNotNone(model_class.component) | ||||||
|  |         _ = model_class.ui_login_button | ||||||
|  |         _ = model_class.ui_user_settings | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def provider_tester_factory(test_model: Type[Stage]) -> Callable: | ||||||
|  |     """Test provider""" | ||||||
|  |  | ||||||
|  |     def tester(self: TestModels): | ||||||
|  |         model_class = None | ||||||
|  |         if test_model._meta.abstract: | ||||||
|  |             model_class = test_model.__bases__[0]() | ||||||
|  |         else: | ||||||
|  |             model_class = test_model() | ||||||
|  |         self.assertIsNotNone(model_class.component) | ||||||
|  |  | ||||||
|  |     return tester | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for model in all_subclasses(Source): | ||||||
|  |     setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model)) | ||||||
|  | for model in all_subclasses(Provider): | ||||||
|  |     setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model)) | ||||||
|  | |||||||
| @ -2,9 +2,10 @@ | |||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| from rest_framework.fields import CharField | from rest_framework.fields import CharField, DictField | ||||||
|  |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
|  | from authentik.flows.challenge import Challenge | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| @ -14,8 +15,8 @@ class UILoginButton: | |||||||
|     # Name, ran through i18n |     # Name, ran through i18n | ||||||
|     name: str |     name: str | ||||||
|  |  | ||||||
|     # URL Which Button points to |     # Challenge which is presented to the user when they click the button | ||||||
|     url: str |     challenge: Challenge | ||||||
|  |  | ||||||
|     # Icon URL, used as-is |     # Icon URL, used as-is | ||||||
|     icon_url: Optional[str] = None |     icon_url: Optional[str] = None | ||||||
| @ -25,7 +26,7 @@ class UILoginButtonSerializer(PassiveSerializer): | |||||||
|     """Serializer for Login buttons of sources""" |     """Serializer for Login buttons of sources""" | ||||||
|  |  | ||||||
|     name = CharField() |     name = CharField() | ||||||
|     url = CharField() |     challenge = DictField() | ||||||
|     icon_url = CharField(required=False, allow_null=True) |     icon_url = CharField(required=False, allow_null=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -35,7 +35,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str): | |||||||
|         LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) |         LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) | ||||||
|         return |         return | ||||||
|     event: Event = events.first() |     event: Event = events.first() | ||||||
|     trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name) |     triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name) | ||||||
|  |     if not triggers.exists(): | ||||||
|  |         return | ||||||
|  |     trigger = triggers.first() | ||||||
|  |  | ||||||
|     if "policy_uuid" in event.context: |     if "policy_uuid" in event.context: | ||||||
|         policy_uuid = event.context["policy_uuid"] |         policy_uuid = event.context["policy_uuid"] | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable: | |||||||
|     """Test a form""" |     """Test a form""" | ||||||
|  |  | ||||||
|     def tester(self: TestModels): |     def tester(self: TestModels): | ||||||
|         try: |  | ||||||
|         model_class = None |         model_class = None | ||||||
|         if test_model._meta.abstract: |         if test_model._meta.abstract: | ||||||
|             model_class = test_model.__bases__[0]() |             model_class = test_model.__bases__[0]() | ||||||
| @ -25,8 +24,6 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable: | |||||||
|         self.assertTrue(issubclass(model_class.type, StageView)) |         self.assertTrue(issubclass(model_class.type, StageView)) | ||||||
|         self.assertIsNotNone(test_model.component) |         self.assertIsNotNone(test_model.component) | ||||||
|         _ = test_model.ui_user_settings |         _ = test_model.ui_user_settings | ||||||
|         except NotImplementedError: |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|     return tester |     return tester | ||||||
|  |  | ||||||
|  | |||||||
| @ -0,0 +1,84 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-05-02 17:06 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_policies_event_matcher", "0012_auto_20210323_1339"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="eventmatcherpolicy", | ||||||
|  |             name="app", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 blank=True, | ||||||
|  |                 choices=[ | ||||||
|  |                     ("authentik.admin", "authentik Admin"), | ||||||
|  |                     ("authentik.api", "authentik API"), | ||||||
|  |                     ("authentik.events", "authentik Events"), | ||||||
|  |                     ("authentik.crypto", "authentik Crypto"), | ||||||
|  |                     ("authentik.flows", "authentik Flows"), | ||||||
|  |                     ("authentik.outposts", "authentik Outpost"), | ||||||
|  |                     ("authentik.lib", "authentik lib"), | ||||||
|  |                     ("authentik.policies", "authentik Policies"), | ||||||
|  |                     ("authentik.policies.dummy", "authentik Policies.Dummy"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.policies.event_matcher", | ||||||
|  |                         "authentik Policies.Event Matcher", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.policies.expiry", "authentik Policies.Expiry"), | ||||||
|  |                     ("authentik.policies.expression", "authentik Policies.Expression"), | ||||||
|  |                     ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"), | ||||||
|  |                     ("authentik.policies.password", "authentik Policies.Password"), | ||||||
|  |                     ("authentik.policies.reputation", "authentik Policies.Reputation"), | ||||||
|  |                     ("authentik.providers.proxy", "authentik Providers.Proxy"), | ||||||
|  |                     ("authentik.providers.oauth2", "authentik Providers.OAuth2"), | ||||||
|  |                     ("authentik.providers.saml", "authentik Providers.SAML"), | ||||||
|  |                     ("authentik.recovery", "authentik Recovery"), | ||||||
|  |                     ("authentik.sources.ldap", "authentik Sources.LDAP"), | ||||||
|  |                     ("authentik.sources.oauth", "authentik Sources.OAuth"), | ||||||
|  |                     ("authentik.sources.plex", "authentik Sources.Plex"), | ||||||
|  |                     ("authentik.sources.saml", "authentik Sources.SAML"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_static", | ||||||
|  |                         "authentik Stages.Authenticator.Static", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_totp", | ||||||
|  |                         "authentik Stages.Authenticator.TOTP", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_validate", | ||||||
|  |                         "authentik Stages.Authenticator.Validate", | ||||||
|  |                     ), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.authenticator_webauthn", | ||||||
|  |                         "authentik Stages.Authenticator.WebAuthn", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.stages.captcha", "authentik Stages.Captcha"), | ||||||
|  |                     ("authentik.stages.consent", "authentik Stages.Consent"), | ||||||
|  |                     ("authentik.stages.deny", "authentik Stages.Deny"), | ||||||
|  |                     ("authentik.stages.dummy", "authentik Stages.Dummy"), | ||||||
|  |                     ("authentik.stages.email", "authentik Stages.Email"), | ||||||
|  |                     ( | ||||||
|  |                         "authentik.stages.identification", | ||||||
|  |                         "authentik Stages.Identification", | ||||||
|  |                     ), | ||||||
|  |                     ("authentik.stages.invitation", "authentik Stages.User Invitation"), | ||||||
|  |                     ("authentik.stages.password", "authentik Stages.Password"), | ||||||
|  |                     ("authentik.stages.prompt", "authentik Stages.Prompt"), | ||||||
|  |                     ("authentik.stages.user_delete", "authentik Stages.User Delete"), | ||||||
|  |                     ("authentik.stages.user_login", "authentik Stages.User Login"), | ||||||
|  |                     ("authentik.stages.user_logout", "authentik Stages.User Logout"), | ||||||
|  |                     ("authentik.stages.user_write", "authentik Stages.User Write"), | ||||||
|  |                     ("authentik.core", "authentik Core"), | ||||||
|  |                     ("authentik.managed", "authentik Managed"), | ||||||
|  |                 ], | ||||||
|  |                 default="", | ||||||
|  |                 help_text="Match events created by selected application. When left empty, all applications are matched.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -107,6 +107,7 @@ INSTALLED_APPS = [ | |||||||
|     "authentik.recovery", |     "authentik.recovery", | ||||||
|     "authentik.sources.ldap", |     "authentik.sources.ldap", | ||||||
|     "authentik.sources.oauth", |     "authentik.sources.oauth", | ||||||
|  |     "authentik.sources.plex", | ||||||
|     "authentik.sources.saml", |     "authentik.sources.saml", | ||||||
|     "authentik.stages.authenticator_static", |     "authentik.stages.authenticator_static", | ||||||
|     "authentik.stages.authenticator_totp", |     "authentik.stages.authenticator_totp", | ||||||
|  | |||||||
| @ -2,11 +2,21 @@ | |||||||
| from importlib import import_module | from importlib import import_module | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
| from django.conf import settings |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | AUTHENTIK_SOURCES_OAUTH_TYPES = [ | ||||||
|  |     "authentik.sources.oauth.types.discord", | ||||||
|  |     "authentik.sources.oauth.types.facebook", | ||||||
|  |     "authentik.sources.oauth.types.github", | ||||||
|  |     "authentik.sources.oauth.types.google", | ||||||
|  |     "authentik.sources.oauth.types.reddit", | ||||||
|  |     "authentik.sources.oauth.types.twitter", | ||||||
|  |     "authentik.sources.oauth.types.azure_ad", | ||||||
|  |     "authentik.sources.oauth.types.oidc", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikSourceOAuthConfig(AppConfig): | class AuthentikSourceOAuthConfig(AppConfig): | ||||||
|     """authentik source.oauth config""" |     """authentik source.oauth config""" | ||||||
| @ -18,7 +28,7 @@ class AuthentikSourceOAuthConfig(AppConfig): | |||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|         """Load source_types from config file""" |         """Load source_types from config file""" | ||||||
|         for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES: |         for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES: | ||||||
|             try: |             try: | ||||||
|                 import_module(source_type) |                 import_module(source_type) | ||||||
|                 LOGGER.debug("Loaded OAuth Source Type", type=source_type) |                 LOGGER.debug("Loaded OAuth Source Type", type=source_type) | ||||||
|  | |||||||
| @ -1,23 +0,0 @@ | |||||||
| """authentik oauth_client Authorization backend""" |  | ||||||
| from typing import Optional |  | ||||||
|  |  | ||||||
| from django.contrib.auth.backends import ModelBackend |  | ||||||
| from django.http import HttpRequest |  | ||||||
|  |  | ||||||
| from authentik.core.models import User |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthorizedServiceBackend(ModelBackend): |  | ||||||
|     "Authentication backend for users registered with remote OAuth provider." |  | ||||||
|  |  | ||||||
|     def authenticate( |  | ||||||
|         self, request: HttpRequest, source: OAuthSource, identifier: str |  | ||||||
|     ) -> Optional[User]: |  | ||||||
|         "Fetch user for a given source by id." |  | ||||||
|         access = UserOAuthSourceConnection.objects.filter( |  | ||||||
|             source=source, identifier=identifier |  | ||||||
|         ).select_related("user") |  | ||||||
|         if not access.exists(): |  | ||||||
|             return None |  | ||||||
|         return access.first().user |  | ||||||
| @ -9,6 +9,7 @@ from rest_framework.serializers import Serializer | |||||||
|  |  | ||||||
| from authentik.core.models import Source, UserSourceConnection | from authentik.core.models import Source, UserSourceConnection | ||||||
| from authentik.core.types import UILoginButton, UserSettingSerializer | from authentik.core.types import UILoginButton, UserSettingSerializer | ||||||
|  | from authentik.flows.challenge import ChallengeTypes, RedirectChallenge | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from authentik.sources.oauth.types.manager import SourceType |     from authentik.sources.oauth.types.manager import SourceType | ||||||
| @ -67,10 +68,15 @@ class OAuthSource(Source): | |||||||
|     @property |     @property | ||||||
|     def ui_login_button(self) -> UILoginButton: |     def ui_login_button(self) -> UILoginButton: | ||||||
|         return UILoginButton( |         return UILoginButton( | ||||||
|             url=reverse( |             challenge=RedirectChallenge( | ||||||
|  |                 instance={ | ||||||
|  |                     "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                     "to": reverse( | ||||||
|                         "authentik_sources_oauth:oauth-client-login", |                         "authentik_sources_oauth:oauth-client-login", | ||||||
|                         kwargs={"source_slug": self.slug}, |                         kwargs={"source_slug": self.slug}, | ||||||
|                     ), |                     ), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|             icon_url=static(f"authentik/sources/{self.provider_type}.svg"), |             icon_url=static(f"authentik/sources/{self.provider_type}.svg"), | ||||||
|             name=self.name, |             name=self.name, | ||||||
|         ) |         ) | ||||||
| @ -163,16 +169,6 @@ class OpenIDOAuthSource(OAuthSource): | |||||||
|         verbose_name_plural = _("OpenID OAuth Sources") |         verbose_name_plural = _("OpenID OAuth Sources") | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlexOAuthSource(OAuthSource): |  | ||||||
|     """Login using plex.tv.""" |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|  |  | ||||||
|         abstract = True |  | ||||||
|         verbose_name = _("Plex OAuth Source") |  | ||||||
|         verbose_name_plural = _("Plex OAuth Sources") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserOAuthSourceConnection(UserSourceConnection): | class UserOAuthSourceConnection(UserSourceConnection): | ||||||
|     """Authorized remote OAuth provider.""" |     """Authorized remote OAuth provider.""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,13 +0,0 @@ | |||||||
| """Oauth2 Client Settings""" |  | ||||||
|  |  | ||||||
| AUTHENTIK_SOURCES_OAUTH_TYPES = [ |  | ||||||
|     "authentik.sources.oauth.types.discord", |  | ||||||
|     "authentik.sources.oauth.types.facebook", |  | ||||||
|     "authentik.sources.oauth.types.github", |  | ||||||
|     "authentik.sources.oauth.types.google", |  | ||||||
|     "authentik.sources.oauth.types.reddit", |  | ||||||
|     "authentik.sources.oauth.types.twitter", |  | ||||||
|     "authentik.sources.oauth.types.azure_ad", |  | ||||||
|     "authentik.sources.oauth.types.oidc", |  | ||||||
|     "authentik.sources.oauth.types.plex", |  | ||||||
| ] |  | ||||||
| @ -1,7 +1,7 @@ | |||||||
| """Discord Type tests""" | """Discord Type tests""" | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.sources.oauth.types.discord import DiscordOAuth2Callback | from authentik.sources.oauth.types.discord import DiscordOAuth2Callback | ||||||
|  |  | ||||||
| # https://discord.com/developers/docs/resources/user#user-object | # https://discord.com/developers/docs/resources/user#user-object | ||||||
| @ -33,9 +33,7 @@ class TestTypeDiscord(TestCase): | |||||||
|  |  | ||||||
|     def test_enroll_context(self): |     def test_enroll_context(self): | ||||||
|         """Test discord Enrollment context""" |         """Test discord Enrollment context""" | ||||||
|         ak_context = DiscordOAuth2Callback().get_user_enroll_context( |         ak_context = DiscordOAuth2Callback().get_user_enroll_context(DISCORD_USER) | ||||||
|             self.source, UserOAuthSourceConnection(), DISCORD_USER |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(ak_context["username"], DISCORD_USER["username"]) |         self.assertEqual(ak_context["username"], DISCORD_USER["username"]) | ||||||
|         self.assertEqual(ak_context["email"], DISCORD_USER["email"]) |         self.assertEqual(ak_context["email"], DISCORD_USER["email"]) | ||||||
|         self.assertEqual(ak_context["name"], DISCORD_USER["username"]) |         self.assertEqual(ak_context["name"], DISCORD_USER["username"]) | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """GitHub Type tests""" | """GitHub Type tests""" | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.sources.oauth.types.github import GitHubOAuth2Callback | from authentik.sources.oauth.types.github import GitHubOAuth2Callback | ||||||
|  |  | ||||||
| # https://developer.github.com/v3/users/#get-the-authenticated-user | # https://developer.github.com/v3/users/#get-the-authenticated-user | ||||||
| @ -63,9 +63,7 @@ class TestTypeGitHub(TestCase): | |||||||
|  |  | ||||||
|     def test_enroll_context(self): |     def test_enroll_context(self): | ||||||
|         """Test GitHub Enrollment context""" |         """Test GitHub Enrollment context""" | ||||||
|         ak_context = GitHubOAuth2Callback().get_user_enroll_context( |         ak_context = GitHubOAuth2Callback().get_user_enroll_context(GITHUB_USER) | ||||||
|             self.source, UserOAuthSourceConnection(), GITHUB_USER |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(ak_context["username"], GITHUB_USER["login"]) |         self.assertEqual(ak_context["username"], GITHUB_USER["login"]) | ||||||
|         self.assertEqual(ak_context["email"], GITHUB_USER["email"]) |         self.assertEqual(ak_context["email"], GITHUB_USER["email"]) | ||||||
|         self.assertEqual(ak_context["name"], GITHUB_USER["name"]) |         self.assertEqual(ak_context["name"], GITHUB_USER["name"]) | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """google Type tests""" | """google Type tests""" | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.sources.oauth.types.google import GoogleOAuth2Callback | from authentik.sources.oauth.types.google import GoogleOAuth2Callback | ||||||
|  |  | ||||||
| # https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en | # https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en | ||||||
| @ -32,9 +32,7 @@ class TestTypeGoogle(TestCase): | |||||||
|  |  | ||||||
|     def test_enroll_context(self): |     def test_enroll_context(self): | ||||||
|         """Test Google Enrollment context""" |         """Test Google Enrollment context""" | ||||||
|         ak_context = GoogleOAuth2Callback().get_user_enroll_context( |         ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER) | ||||||
|             self.source, UserOAuthSourceConnection(), GOOGLE_USER |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(ak_context["username"], GOOGLE_USER["email"]) |         self.assertEqual(ak_context["username"], GOOGLE_USER["email"]) | ||||||
|         self.assertEqual(ak_context["email"], GOOGLE_USER["email"]) |         self.assertEqual(ak_context["email"], GOOGLE_USER["email"]) | ||||||
|         self.assertEqual(ak_context["name"], GOOGLE_USER["name"]) |         self.assertEqual(ak_context["name"], GOOGLE_USER["name"]) | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """Twitter Type tests""" | """Twitter Type tests""" | ||||||
| from django.test import Client, TestCase | from django.test import Client, TestCase | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.sources.oauth.types.twitter import TwitterOAuthCallback | from authentik.sources.oauth.types.twitter import TwitterOAuthCallback | ||||||
|  |  | ||||||
| # https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \ | # https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \ | ||||||
| @ -104,9 +104,7 @@ class TestTypeGitHub(TestCase): | |||||||
|  |  | ||||||
|     def test_enroll_context(self): |     def test_enroll_context(self): | ||||||
|         """Test Twitter Enrollment context""" |         """Test Twitter Enrollment context""" | ||||||
|         ak_context = TwitterOAuthCallback().get_user_enroll_context( |         ak_context = TwitterOAuthCallback().get_user_enroll_context(TWITTER_USER) | ||||||
|             self.source, UserOAuthSourceConnection(), TWITTER_USER |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"]) |         self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"]) | ||||||
|         self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None)) |         self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None)) | ||||||
|         self.assertEqual(ak_context["name"], TWITTER_USER["name"]) |         self.assertEqual(ak_context["name"], TWITTER_USER["name"]) | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ | |||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
| from uuid import UUID | from uuid import UUID | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
|  |  | ||||||
| @ -10,7 +9,7 @@ from authentik.sources.oauth.views.callback import OAuthCallback | |||||||
| class AzureADOAuthCallback(OAuthCallback): | class AzureADOAuthCallback(OAuthCallback): | ||||||
|     """AzureAD OAuth2 Callback""" |     """AzureAD OAuth2 Callback""" | ||||||
|  |  | ||||||
|     def get_user_id(self, source: OAuthSource, info: dict[str, Any]) -> Optional[str]: |     def get_user_id(self, info: dict[str, Any]) -> Optional[str]: | ||||||
|         try: |         try: | ||||||
|             return str(UUID(info.get("objectId")).int) |             return str(UUID(info.get("objectId")).int) | ||||||
|         except TypeError: |         except TypeError: | ||||||
| @ -18,8 +17,6 @@ class AzureADOAuthCallback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         mail = info.get("mail", None) or info.get("otherMails", [None])[0] |         mail = info.get("mail", None) or info.get("otherMails", [None])[0] | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """Discord OAuth Views""" | """Discord OAuth Views""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect | from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||||
| @ -21,8 +20,6 @@ class DiscordOAuth2Callback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ from typing import Any, Optional | |||||||
| from facebook import GraphAPI | from facebook import GraphAPI | ||||||
|  |  | ||||||
| from authentik.sources.oauth.clients.oauth2 import OAuth2Client | from authentik.sources.oauth.clients.oauth2 import OAuth2Client | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect | from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||||
| @ -34,8 +33,6 @@ class FacebookOAuth2Callback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """GitHub OAuth Views""" | """GitHub OAuth Views""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
|  |  | ||||||
| @ -11,8 +10,6 @@ class GitHubOAuth2Callback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """Google OAuth Views""" | """Google OAuth Views""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect | from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||||
| @ -21,8 +20,6 @@ class GoogleOAuth2Callback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """OpenID Connect OAuth Views""" | """OpenID Connect OAuth Views""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect | from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||||
| @ -19,13 +19,11 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect): | |||||||
| class OpenIDConnectOAuth2Callback(OAuthCallback): | class OpenIDConnectOAuth2Callback(OAuthCallback): | ||||||
|     """OpenIDConnect OAuth2 Callback""" |     """OpenIDConnect OAuth2 Callback""" | ||||||
|  |  | ||||||
|     def get_user_id(self, source: OAuthSource, info: dict[str, str]) -> str: |     def get_user_id(self, info: dict[str, str]) -> str: | ||||||
|         return info.get("sub", "") |         return info.get("sub", "") | ||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -1,134 +0,0 @@ | |||||||
| """Plex OAuth Views""" |  | ||||||
| from typing import Any, Optional |  | ||||||
| from urllib.parse import urlencode |  | ||||||
|  |  | ||||||
| from django.http.response import Http404 |  | ||||||
| from requests import post |  | ||||||
| from requests.api import get |  | ||||||
| from requests.exceptions import RequestException |  | ||||||
| from structlog.stdlib import get_logger |  | ||||||
|  |  | ||||||
| from authentik import __version__ |  | ||||||
| from authentik.sources.oauth.clients.oauth2 import OAuth2Client |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType |  | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback |  | ||||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() |  | ||||||
| SESSION_ID_KEY = "PLEX_ID" |  | ||||||
| SESSION_CODE_KEY = "PLEX_CODE" |  | ||||||
| DEFAULT_PAYLOAD = { |  | ||||||
|     "X-Plex-Product": "authentik", |  | ||||||
|     "X-Plex-Version": __version__, |  | ||||||
|     "X-Plex-Device-Vendor": "BeryJu.org", |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlexRedirect(OAuthRedirect): |  | ||||||
|     """Plex Auth redirect, get a pin then redirect to a URL to claim it""" |  | ||||||
|  |  | ||||||
|     headers = {} |  | ||||||
|  |  | ||||||
|     def get_pin(self, **data) -> dict: |  | ||||||
|         """Get plex pin that the user will claim |  | ||||||
|         https://forums.plex.tv/t/authenticating-with-plex/609370""" |  | ||||||
|         return post( |  | ||||||
|             "https://plex.tv/api/v2/pins.json?strong=true", |  | ||||||
|             data=data, |  | ||||||
|             headers=self.headers, |  | ||||||
|         ).json() |  | ||||||
|  |  | ||||||
|     def get_redirect_url(self, **kwargs) -> str: |  | ||||||
|         slug = kwargs.get("source_slug", "") |  | ||||||
|         self.headers = {"Origin": self.request.build_absolute_uri("/")} |  | ||||||
|         try: |  | ||||||
|             source: OAuthSource = OAuthSource.objects.get(slug=slug) |  | ||||||
|         except OAuthSource.DoesNotExist: |  | ||||||
|             raise Http404(f"Unknown OAuth source '{slug}'.") |  | ||||||
|         else: |  | ||||||
|             payload = DEFAULT_PAYLOAD.copy() |  | ||||||
|             payload["X-Plex-Client-Identifier"] = source.consumer_key |  | ||||||
|             # Get a pin first |  | ||||||
|             pin = self.get_pin(**payload) |  | ||||||
|             LOGGER.debug("Got pin", **pin) |  | ||||||
|             self.request.session[SESSION_ID_KEY] = pin["id"] |  | ||||||
|             self.request.session[SESSION_CODE_KEY] = pin["code"] |  | ||||||
|             qs = { |  | ||||||
|                 "clientID": source.consumer_key, |  | ||||||
|                 "code": pin["code"], |  | ||||||
|                 "forwardUrl": self.request.build_absolute_uri( |  | ||||||
|                     self.get_callback_url(source) |  | ||||||
|                 ), |  | ||||||
|             } |  | ||||||
|             return f"https://app.plex.tv/auth#!?{urlencode(qs)}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlexOAuthClient(OAuth2Client): |  | ||||||
|     """Retrive the plex token after authentication, then ask the plex API about user info""" |  | ||||||
|  |  | ||||||
|     def check_application_state(self) -> bool: |  | ||||||
|         return SESSION_ID_KEY in self.request.session |  | ||||||
|  |  | ||||||
|     def get_access_token(self, **request_kwargs) -> Optional[dict[str, Any]]: |  | ||||||
|         payload = dict(DEFAULT_PAYLOAD) |  | ||||||
|         payload["X-Plex-Client-Identifier"] = self.source.consumer_key |  | ||||||
|         payload["Accept"] = "application/json" |  | ||||||
|         response = get( |  | ||||||
|             f"https://plex.tv/api/v2/pins/{self.request.session[SESSION_ID_KEY]}", |  | ||||||
|             headers=payload, |  | ||||||
|         ) |  | ||||||
|         response.raise_for_status() |  | ||||||
|         token = response.json()["authToken"] |  | ||||||
|         return {"plex_token": token} |  | ||||||
|  |  | ||||||
|     def get_profile_info(self, token: dict[str, str]) -> Optional[dict[str, Any]]: |  | ||||||
|         "Fetch user profile information." |  | ||||||
|         qs = {"X-Plex-Token": token["plex_token"]} |  | ||||||
|         try: |  | ||||||
|             response = self.do_request( |  | ||||||
|                 "get", f"https://plex.tv/users/account.json?{urlencode(qs)}" |  | ||||||
|             ) |  | ||||||
|             response.raise_for_status() |  | ||||||
|         except RequestException as exc: |  | ||||||
|             LOGGER.warning("Unable to fetch user profile", exc=exc) |  | ||||||
|             return None |  | ||||||
|         else: |  | ||||||
|             return response.json().get("user", {}) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PlexOAuth2Callback(OAuthCallback): |  | ||||||
|     """Plex OAuth2 Callback""" |  | ||||||
|  |  | ||||||
|     client_class = PlexOAuthClient |  | ||||||
|  |  | ||||||
|     def get_user_id( |  | ||||||
|         self, source: UserOAuthSourceConnection, info: dict[str, Any] |  | ||||||
|     ) -> Optional[str]: |  | ||||||
|         return info.get("uuid") |  | ||||||
|  |  | ||||||
|     def get_user_enroll_context( |  | ||||||
|         self, |  | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |  | ||||||
|     ) -> dict[str, Any]: |  | ||||||
|         return { |  | ||||||
|             "username": info.get("username"), |  | ||||||
|             "email": info.get("email"), |  | ||||||
|             "name": info.get("title"), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @MANAGER.type() |  | ||||||
| class PlexType(SourceType): |  | ||||||
|     """Plex Type definition""" |  | ||||||
|  |  | ||||||
|     redirect_view = PlexRedirect |  | ||||||
|     callback_view = PlexOAuth2Callback |  | ||||||
|     name = "Plex" |  | ||||||
|     slug = "plex" |  | ||||||
|  |  | ||||||
|     authorization_url = "" |  | ||||||
|     access_token_url = ""  # nosec |  | ||||||
|     profile_url = "" |  | ||||||
| @ -4,7 +4,6 @@ from typing import Any | |||||||
| from requests.auth import HTTPBasicAuth | from requests.auth import HTTPBasicAuth | ||||||
|  |  | ||||||
| from authentik.sources.oauth.clients.oauth2 import OAuth2Client | from authentik.sources.oauth.clients.oauth2 import OAuth2Client | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.sources.oauth.views.redirect import OAuthRedirect | from authentik.sources.oauth.views.redirect import OAuthRedirect | ||||||
| @ -36,8 +35,6 @@ class RedditOAuth2Callback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """Twitter OAuth Views""" | """Twitter OAuth Views""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection |  | ||||||
| from authentik.sources.oauth.types.manager import MANAGER, SourceType | from authentik.sources.oauth.types.manager import MANAGER, SourceType | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
|  |  | ||||||
| @ -11,8 +10,6 @@ class TwitterOAuthCallback(OAuthCallback): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -4,35 +4,14 @@ from typing import Any, Optional | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib import messages | from django.contrib import messages | ||||||
| from django.http import Http404, HttpRequest, HttpResponse | from django.http import Http404, HttpRequest, HttpResponse | ||||||
| from django.http.response import HttpResponseBadRequest |  | ||||||
| from django.shortcuts import redirect | from django.shortcuts import redirect | ||||||
| from django.urls import reverse |  | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from django.views.generic import View | from django.views.generic import View | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.sources.flow_manager import SourceFlowManager | ||||||
| from authentik.events.models import Event, EventAction |  | ||||||
| from authentik.flows.models import Flow, in_memory_stage |  | ||||||
| from authentik.flows.planner import ( |  | ||||||
|     PLAN_CONTEXT_PENDING_USER, |  | ||||||
|     PLAN_CONTEXT_REDIRECT, |  | ||||||
|     PLAN_CONTEXT_SOURCE, |  | ||||||
|     PLAN_CONTEXT_SSO, |  | ||||||
|     FlowPlanner, |  | ||||||
| ) |  | ||||||
| from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN |  | ||||||
| from authentik.lib.utils.urls import redirect_with_qs |  | ||||||
| from authentik.policies.utils import delete_none_keys |  | ||||||
| from authentik.sources.oauth.auth import AuthorizedServiceBackend |  | ||||||
| from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection | ||||||
| from authentik.sources.oauth.views.base import OAuthClientMixin | from authentik.sources.oauth.views.base import OAuthClientMixin | ||||||
| from authentik.sources.oauth.views.flows import ( |  | ||||||
|     PLAN_CONTEXT_SOURCES_OAUTH_ACCESS, |  | ||||||
|     PostUserEnrollmentStage, |  | ||||||
| ) |  | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND |  | ||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| @ -40,8 +19,7 @@ LOGGER = get_logger() | |||||||
| class OAuthCallback(OAuthClientMixin, View): | class OAuthCallback(OAuthClientMixin, View): | ||||||
|     "Base OAuth callback view." |     "Base OAuth callback view." | ||||||
|  |  | ||||||
|     source_id = None |     source: OAuthSource | ||||||
|     source = None |  | ||||||
|  |  | ||||||
|     # pylint: disable=too-many-return-statements |     # pylint: disable=too-many-return-statements | ||||||
|     def get(self, request: HttpRequest, *_, **kwargs) -> HttpResponse: |     def get(self, request: HttpRequest, *_, **kwargs) -> HttpResponse: | ||||||
| @ -60,47 +38,27 @@ class OAuthCallback(OAuthClientMixin, View): | |||||||
|         # Fetch access token |         # Fetch access token | ||||||
|         token = client.get_access_token() |         token = client.get_access_token() | ||||||
|         if token is None: |         if token is None: | ||||||
|             return self.handle_login_failure(self.source, "Could not retrieve token.") |             return self.handle_login_failure("Could not retrieve token.") | ||||||
|         if "error" in token: |         if "error" in token: | ||||||
|             return self.handle_login_failure(self.source, token["error"]) |             return self.handle_login_failure(token["error"]) | ||||||
|         # Fetch profile info |         # Fetch profile info | ||||||
|         info = client.get_profile_info(token) |         raw_info = client.get_profile_info(token) | ||||||
|         if info is None: |         if raw_info is None: | ||||||
|             return self.handle_login_failure(self.source, "Could not retrieve profile.") |             return self.handle_login_failure("Could not retrieve profile.") | ||||||
|         identifier = self.get_user_id(self.source, info) |         identifier = self.get_user_id(raw_info) | ||||||
|         if identifier is None: |         if identifier is None: | ||||||
|             return self.handle_login_failure(self.source, "Could not determine id.") |             return self.handle_login_failure("Could not determine id.") | ||||||
|         # Get or create access record |         # Get or create access record | ||||||
|         defaults = { |         enroll_info = self.get_user_enroll_context(raw_info) | ||||||
|             "access_token": token.get("access_token"), |         sfm = OAuthSourceFlowManager( | ||||||
|         } |  | ||||||
|         existing = UserOAuthSourceConnection.objects.filter( |  | ||||||
|             source=self.source, identifier=identifier |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if existing.exists(): |  | ||||||
|             connection = existing.first() |  | ||||||
|             connection.access_token = token.get("access_token") |  | ||||||
|             UserOAuthSourceConnection.objects.filter(pk=connection.pk).update( |  | ||||||
|                 **defaults |  | ||||||
|             ) |  | ||||||
|         else: |  | ||||||
|             connection = UserOAuthSourceConnection( |  | ||||||
|             source=self.source, |             source=self.source, | ||||||
|  |             request=self.request, | ||||||
|             identifier=identifier, |             identifier=identifier, | ||||||
|  |             enroll_info=enroll_info, | ||||||
|  |         ) | ||||||
|  |         return sfm.get_flow( | ||||||
|             access_token=token.get("access_token"), |             access_token=token.get("access_token"), | ||||||
|         ) |         ) | ||||||
|         user = AuthorizedServiceBackend().authenticate( |  | ||||||
|             source=self.source, identifier=identifier, request=request |  | ||||||
|         ) |  | ||||||
|         if user is None: |  | ||||||
|             if self.request.user.is_authenticated: |  | ||||||
|                 LOGGER.debug("Linking existing user", source=self.source) |  | ||||||
|                 return self.handle_existing_user_link(self.source, connection, info) |  | ||||||
|             LOGGER.debug("Handling enrollment of new user", source=self.source) |  | ||||||
|             return self.handle_enroll(self.source, connection, info) |  | ||||||
|         LOGGER.debug("Handling existing user", source=self.source) |  | ||||||
|         return self.handle_existing_user(self.source, user, connection, info) |  | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument |     # pylint: disable=unused-argument | ||||||
|     def get_callback_url(self, source: OAuthSource) -> str: |     def get_callback_url(self, source: OAuthSource) -> str: | ||||||
| @ -114,132 +72,35 @@ class OAuthCallback(OAuthClientMixin, View): | |||||||
|  |  | ||||||
|     def get_user_enroll_context( |     def get_user_enroll_context( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |         info: dict[str, Any], | ||||||
|     ) -> dict[str, Any]: |     ) -> dict[str, Any]: | ||||||
|         """Create a dict of User data""" |         """Create a dict of User data""" | ||||||
|         raise NotImplementedError() |         raise NotImplementedError() | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument |     # pylint: disable=unused-argument | ||||||
|     def get_user_id( |     def get_user_id(self, info: dict[str, Any]) -> Optional[str]: | ||||||
|         self, source: UserOAuthSourceConnection, info: dict[str, Any] |  | ||||||
|     ) -> Optional[str]: |  | ||||||
|         """Return unique identifier from the profile info.""" |         """Return unique identifier from the profile info.""" | ||||||
|         if "id" in info: |         if "id" in info: | ||||||
|             return info["id"] |             return info["id"] | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     def handle_login_failure(self, source: OAuthSource, reason: str) -> HttpResponse: |     def handle_login_failure(self, reason: str) -> HttpResponse: | ||||||
|         "Message user and redirect on error." |         "Message user and redirect on error." | ||||||
|         LOGGER.warning("Authentication Failure", reason=reason) |         LOGGER.warning("Authentication Failure", reason=reason) | ||||||
|         messages.error(self.request, _("Authentication Failed.")) |         messages.error(self.request, _("Authentication Failed.")) | ||||||
|         return redirect(self.get_error_redirect(source, reason)) |         return redirect(self.get_error_redirect(self.source, reason)) | ||||||
|  |  | ||||||
|     def handle_login_flow( |  | ||||||
|         self, flow: Flow, *stages_to_append, **kwargs |  | ||||||
|     ) -> HttpResponse: |  | ||||||
|         """Prepare Authentication Plan, redirect user FlowExecutor""" |  | ||||||
|         # Ensure redirect is carried through when user was trying to |  | ||||||
|         # authorize application |  | ||||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( |  | ||||||
|             NEXT_ARG_NAME, "authentik_core:if-admin" |  | ||||||
|         ) |  | ||||||
|         kwargs.update( |  | ||||||
|             { |  | ||||||
|                 # Since we authenticate the user by their token, they have no backend set |  | ||||||
|                 PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend", |  | ||||||
|                 PLAN_CONTEXT_SSO: True, |  | ||||||
|                 PLAN_CONTEXT_SOURCE: self.source, |  | ||||||
|                 PLAN_CONTEXT_REDIRECT: final_redirect, |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|         if not flow: |  | ||||||
|             return HttpResponseBadRequest() |  | ||||||
|         # We run the Flow planner here so we can pass the Pending user in the context |  | ||||||
|         planner = FlowPlanner(flow) |  | ||||||
|         plan = planner.plan(self.request, kwargs) |  | ||||||
|         for stage in stages_to_append: |  | ||||||
|             plan.append(stage) |  | ||||||
|         self.request.session[SESSION_KEY_PLAN] = plan |  | ||||||
|         return redirect_with_qs( |  | ||||||
|             "authentik_core:if-flow", |  | ||||||
|             self.request.GET, |  | ||||||
|             flow_slug=flow.slug, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument | class OAuthSourceFlowManager(SourceFlowManager): | ||||||
|     def handle_existing_user( |     """Flow manager for oauth sources""" | ||||||
|  |  | ||||||
|  |     connection_type = UserOAuthSourceConnection | ||||||
|  |  | ||||||
|  |     def update_connection( | ||||||
|         self, |         self, | ||||||
|         source: OAuthSource, |         connection: UserOAuthSourceConnection, | ||||||
|         user: User, |         access_token: Optional[str] = None, | ||||||
|         access: UserOAuthSourceConnection, |     ) -> UserOAuthSourceConnection: | ||||||
|         info: dict[str, Any], |         """Set the access_token on the connection""" | ||||||
|     ) -> HttpResponse: |         connection.access_token = access_token | ||||||
|         "Login user and redirect." |         return connection | ||||||
|         messages.success( |  | ||||||
|             self.request, |  | ||||||
|             _( |  | ||||||
|                 "Successfully authenticated with %(source)s!" |  | ||||||
|                 % {"source": self.source.name} |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|         flow_kwargs = {PLAN_CONTEXT_PENDING_USER: user} |  | ||||||
|         return self.handle_login_flow(source.authentication_flow, **flow_kwargs) |  | ||||||
|  |  | ||||||
|     def handle_existing_user_link( |  | ||||||
|         self, |  | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |  | ||||||
|     ) -> HttpResponse: |  | ||||||
|         """Handler when the user was already authenticated and linked an external source |  | ||||||
|         to their account.""" |  | ||||||
|         # there's already a user logged in, just link them up |  | ||||||
|         user = self.request.user |  | ||||||
|         access.user = user |  | ||||||
|         access.save() |  | ||||||
|         UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user) |  | ||||||
|         Event.new( |  | ||||||
|             EventAction.SOURCE_LINKED, message="Linked OAuth Source", source=source |  | ||||||
|         ).from_http(self.request) |  | ||||||
|         messages.success( |  | ||||||
|             self.request, |  | ||||||
|             _("Successfully linked %(source)s!" % {"source": self.source.name}), |  | ||||||
|         ) |  | ||||||
|         return redirect( |  | ||||||
|             reverse( |  | ||||||
|                 "authentik_core:if-admin", |  | ||||||
|             ) |  | ||||||
|             + f"#/user;page-{self.source.slug}" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def handle_enroll( |  | ||||||
|         self, |  | ||||||
|         source: OAuthSource, |  | ||||||
|         access: UserOAuthSourceConnection, |  | ||||||
|         info: dict[str, Any], |  | ||||||
|     ) -> HttpResponse: |  | ||||||
|         """User was not authenticated and previous request was not authenticated.""" |  | ||||||
|         messages.success( |  | ||||||
|             self.request, |  | ||||||
|             _( |  | ||||||
|                 "Successfully authenticated with %(source)s!" |  | ||||||
|                 % {"source": self.source.name} |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # We run the Flow planner here so we can pass the Pending user in the context |  | ||||||
|         if not source.enrollment_flow: |  | ||||||
|             LOGGER.warning("source has no enrollment flow", source=source) |  | ||||||
|             return HttpResponseBadRequest() |  | ||||||
|         return self.handle_login_flow( |  | ||||||
|             source.enrollment_flow, |  | ||||||
|             in_memory_stage(PostUserEnrollmentStage), |  | ||||||
|             **{ |  | ||||||
|                 PLAN_CONTEXT_PROMPT: delete_none_keys( |  | ||||||
|                     self.get_user_enroll_context(source, access, info) |  | ||||||
|                 ), |  | ||||||
|                 PLAN_CONTEXT_SOURCES_OAUTH_ACCESS: access, |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								authentik/sources/plex/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/plex/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										75
									
								
								authentik/sources/plex/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								authentik/sources/plex/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | """Plex Source Serializer""" | ||||||
|  | from django.http import Http404 | ||||||
|  | from django.shortcuts import get_object_or_404 | ||||||
|  | from drf_yasg import openapi | ||||||
|  | from drf_yasg.utils import swagger_auto_schema | ||||||
|  | from rest_framework.decorators import action | ||||||
|  | from rest_framework.fields import CharField | ||||||
|  | from rest_framework.permissions import AllowAny | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.response import Response | ||||||
|  | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
|  | from authentik.api.decorators import permission_required | ||||||
|  | from authentik.core.api.sources import SourceSerializer | ||||||
|  | from authentik.core.api.utils import PassiveSerializer | ||||||
|  | from authentik.flows.challenge import RedirectChallenge | ||||||
|  | from authentik.flows.views import to_stage_response | ||||||
|  | from authentik.sources.plex.models import PlexSource | ||||||
|  | from authentik.sources.plex.plex import PlexAuth | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexSourceSerializer(SourceSerializer): | ||||||
|  |     """Plex Source Serializer""" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = PlexSource | ||||||
|  |         fields = SourceSerializer.Meta.fields + ["client_id", "allowed_servers"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexTokenRedeemSerializer(PassiveSerializer): | ||||||
|  |     """Serializer to redeem a plex token""" | ||||||
|  |  | ||||||
|  |     plex_token = CharField() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexSourceViewSet(ModelViewSet): | ||||||
|  |     """Plex source Viewset""" | ||||||
|  |  | ||||||
|  |     queryset = PlexSource.objects.all() | ||||||
|  |     serializer_class = PlexSourceSerializer | ||||||
|  |     lookup_field = "slug" | ||||||
|  |  | ||||||
|  |     @permission_required(None) | ||||||
|  |     @swagger_auto_schema( | ||||||
|  |         request_body=PlexTokenRedeemSerializer(), | ||||||
|  |         responses={200: RedirectChallenge(), 404: "Token not found"}, | ||||||
|  |         manual_parameters=[ | ||||||
|  |             openapi.Parameter( | ||||||
|  |                 name="slug", | ||||||
|  |                 in_=openapi.IN_QUERY, | ||||||
|  |                 type=openapi.TYPE_STRING, | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  |     @action( | ||||||
|  |         methods=["POST"], | ||||||
|  |         detail=False, | ||||||
|  |         pagination_class=None, | ||||||
|  |         filter_backends=[], | ||||||
|  |         permission_classes=[AllowAny], | ||||||
|  |     ) | ||||||
|  |     def redeem_token(self, request: Request) -> Response: | ||||||
|  |         """Redeem a plex token, check it's access to resources against what's allowed | ||||||
|  |         for the source, and redirect to an authentication/enrollment flow.""" | ||||||
|  |         source: PlexSource = get_object_or_404( | ||||||
|  |             PlexSource, slug=request.query_params.get("slug", "") | ||||||
|  |         ) | ||||||
|  |         plex_token = request.data.get("plex_token", None) | ||||||
|  |         if not plex_token: | ||||||
|  |             raise Http404 | ||||||
|  |         auth_api = PlexAuth(source, plex_token) | ||||||
|  |         if not auth_api.check_server_overlap(): | ||||||
|  |             raise Http404 | ||||||
|  |         response = auth_api.get_user_url(request) | ||||||
|  |         return to_stage_response(request, response) | ||||||
							
								
								
									
										10
									
								
								authentik/sources/plex/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								authentik/sources/plex/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | """authentik plex config""" | ||||||
|  | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthentikSourcePlexConfig(AppConfig): | ||||||
|  |     """authentik source plex config""" | ||||||
|  |  | ||||||
|  |     name = "authentik.sources.plex" | ||||||
|  |     label = "authentik_sources_plex" | ||||||
|  |     verbose_name = "authentik Sources.Plex" | ||||||
							
								
								
									
										77
									
								
								authentik/sources/plex/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								authentik/sources/plex/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-05-03 18:59 | ||||||
|  |  | ||||||
|  | import django.contrib.postgres.fields | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     initial = True | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_core", "0020_source_user_matching_mode"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="PlexSource", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "source_ptr", | ||||||
|  |                     models.OneToOneField( | ||||||
|  |                         auto_created=True, | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         parent_link=True, | ||||||
|  |                         primary_key=True, | ||||||
|  |                         serialize=False, | ||||||
|  |                         to="authentik_core.source", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ( | ||||||
|  |                     "client_id", | ||||||
|  |                     models.TextField( | ||||||
|  |                         default="yOuPQQvgNfBGreZZ38WoOY1d3qk3Xso2AuQHi6RG", | ||||||
|  |                         help_text="Client identifier used to talk to Plex.", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ( | ||||||
|  |                     "allowed_servers", | ||||||
|  |                     django.contrib.postgres.fields.ArrayField( | ||||||
|  |                         base_field=models.TextField(), | ||||||
|  |                         default=list, | ||||||
|  |                         help_text="Which servers a user has to be a member of to be granted access. Empty list allows every server.", | ||||||
|  |                         size=None, | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "Plex Source", | ||||||
|  |                 "verbose_name_plural": "Plex Sources", | ||||||
|  |             }, | ||||||
|  |             bases=("authentik_core.source",), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name="PlexSourceConnection", | ||||||
|  |             fields=[ | ||||||
|  |                 ( | ||||||
|  |                     "usersourceconnection_ptr", | ||||||
|  |                     models.OneToOneField( | ||||||
|  |                         auto_created=True, | ||||||
|  |                         on_delete=django.db.models.deletion.CASCADE, | ||||||
|  |                         parent_link=True, | ||||||
|  |                         primary_key=True, | ||||||
|  |                         serialize=False, | ||||||
|  |                         to="authentik_core.usersourceconnection", | ||||||
|  |                     ), | ||||||
|  |                 ), | ||||||
|  |                 ("plex_token", models.TextField()), | ||||||
|  |                 ("identifier", models.TextField()), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 "verbose_name": "User Plex Source Connection", | ||||||
|  |                 "verbose_name_plural": "User Plex Source Connections", | ||||||
|  |             }, | ||||||
|  |             bases=("authentik_core.usersourceconnection",), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										0
									
								
								authentik/sources/plex/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/plex/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										80
									
								
								authentik/sources/plex/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								authentik/sources/plex/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | |||||||
|  | """Plex source""" | ||||||
|  | from django.contrib.postgres.fields import ArrayField | ||||||
|  | from django.db import models | ||||||
|  | from django.templatetags.static import static | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  | from rest_framework.fields import CharField | ||||||
|  | from rest_framework.serializers import BaseSerializer | ||||||
|  |  | ||||||
|  | from authentik.core.models import Source, UserSourceConnection | ||||||
|  | from authentik.core.types import UILoginButton | ||||||
|  | from authentik.flows.challenge import Challenge, ChallengeTypes | ||||||
|  | from authentik.providers.oauth2.generators import generate_client_id | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexAuthenticationChallenge(Challenge): | ||||||
|  |     """Challenge shown to the user in identification stage""" | ||||||
|  |  | ||||||
|  |     client_id = CharField() | ||||||
|  |     slug = CharField() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexSource(Source): | ||||||
|  |     """Authenticate against plex.tv""" | ||||||
|  |  | ||||||
|  |     client_id = models.TextField( | ||||||
|  |         default=generate_client_id(), | ||||||
|  |         help_text=_("Client identifier used to talk to Plex."), | ||||||
|  |     ) | ||||||
|  |     allowed_servers = ArrayField( | ||||||
|  |         models.TextField(), | ||||||
|  |         default=list, | ||||||
|  |         help_text=_( | ||||||
|  |             ( | ||||||
|  |                 "Which servers a user has to be a member of to be granted access. " | ||||||
|  |                 "Empty list allows every server." | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def component(self) -> str: | ||||||
|  |         return "ak-source-plex-form" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> BaseSerializer: | ||||||
|  |         from authentik.sources.plex.api import PlexSourceSerializer | ||||||
|  |  | ||||||
|  |         return PlexSourceSerializer | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def ui_login_button(self) -> UILoginButton: | ||||||
|  |         return UILoginButton( | ||||||
|  |             challenge=PlexAuthenticationChallenge( | ||||||
|  |                 { | ||||||
|  |                     "type": ChallengeTypes.NATIVE.value, | ||||||
|  |                     "component": "ak-flow-sources-plex", | ||||||
|  |                     "client_id": self.client_id, | ||||||
|  |                     "slug": self.slug, | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |             icon_url=static("authentik/sources/plex.svg"), | ||||||
|  |             name=self.name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         verbose_name = _("Plex Source") | ||||||
|  |         verbose_name_plural = _("Plex Sources") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexSourceConnection(UserSourceConnection): | ||||||
|  |     """Connect user and plex source""" | ||||||
|  |  | ||||||
|  |     plex_token = models.TextField() | ||||||
|  |     identifier = models.TextField() | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         verbose_name = _("User Plex Source Connection") | ||||||
|  |         verbose_name_plural = _("User Plex Source Connections") | ||||||
							
								
								
									
										112
									
								
								authentik/sources/plex/plex.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								authentik/sources/plex/plex.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | |||||||
|  | """Plex Views""" | ||||||
|  | from urllib.parse import urlencode | ||||||
|  |  | ||||||
|  | from django.http.request import HttpRequest | ||||||
|  | from django.http.response import Http404, HttpResponse | ||||||
|  | from requests import Session | ||||||
|  | from requests.exceptions import RequestException | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik import __version__ | ||||||
|  | from authentik.core.sources.flow_manager import SourceFlowManager | ||||||
|  | from authentik.sources.plex.models import PlexSource, PlexSourceConnection | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  | SESSION_ID_KEY = "PLEX_ID" | ||||||
|  | SESSION_CODE_KEY = "PLEX_CODE" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexAuth: | ||||||
|  |     """Plex authentication utilities""" | ||||||
|  |  | ||||||
|  |     _source: PlexSource | ||||||
|  |     _token: str | ||||||
|  |  | ||||||
|  |     def __init__(self, source: PlexSource, token: str): | ||||||
|  |         self._source = source | ||||||
|  |         self._token = token | ||||||
|  |         self._session = Session() | ||||||
|  |         self._session.headers.update( | ||||||
|  |             {"Accept": "application/json", "Content-Type": "application/json"} | ||||||
|  |         ) | ||||||
|  |         self._session.headers.update(self.headers) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def headers(self) -> dict[str, str]: | ||||||
|  |         """Get common headers""" | ||||||
|  |         return { | ||||||
|  |             "X-Plex-Product": "authentik", | ||||||
|  |             "X-Plex-Version": __version__, | ||||||
|  |             "X-Plex-Device-Vendor": "BeryJu.org", | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def get_resources(self) -> list[dict]: | ||||||
|  |         """Get all resources the plex-token has access to""" | ||||||
|  |         qs = { | ||||||
|  |             "X-Plex-Token": self._token, | ||||||
|  |             "X-Plex-Client-Identifier": self._source.client_id, | ||||||
|  |         } | ||||||
|  |         response = self._session.get( | ||||||
|  |             f"https://plex.tv/api/v2/resources?{urlencode(qs)}", | ||||||
|  |         ) | ||||||
|  |         response.raise_for_status() | ||||||
|  |         return response.json() | ||||||
|  |  | ||||||
|  |     def get_user_info(self) -> tuple[dict, int]: | ||||||
|  |         """Get user info of the plex token""" | ||||||
|  |         qs = { | ||||||
|  |             "X-Plex-Token": self._token, | ||||||
|  |             "X-Plex-Client-Identifier": self._source.client_id, | ||||||
|  |         } | ||||||
|  |         response = self._session.get( | ||||||
|  |             f"https://plex.tv/api/v2/user?{urlencode(qs)}", | ||||||
|  |         ) | ||||||
|  |         response.raise_for_status() | ||||||
|  |         raw_user_info = response.json() | ||||||
|  |         return { | ||||||
|  |             "username": raw_user_info.get("username"), | ||||||
|  |             "email": raw_user_info.get("email"), | ||||||
|  |             "name": raw_user_info.get("title"), | ||||||
|  |         }, raw_user_info.get("id") | ||||||
|  |  | ||||||
|  |     def check_server_overlap(self) -> bool: | ||||||
|  |         """Check if the plex-token has any server overlap with our configured servers""" | ||||||
|  |         try: | ||||||
|  |             resources = self.get_resources() | ||||||
|  |         except RequestException as exc: | ||||||
|  |             LOGGER.warning("Unable to fetch user resources", exc=exc) | ||||||
|  |             raise Http404 | ||||||
|  |         else: | ||||||
|  |             for resource in resources: | ||||||
|  |                 if resource["provides"] != "server": | ||||||
|  |                     continue | ||||||
|  |                 if resource["clientIdentifier"] in self._source.allowed_servers: | ||||||
|  |                     LOGGER.info( | ||||||
|  |                         "Plex allowed access from server", name=resource["name"] | ||||||
|  |                     ) | ||||||
|  |                     return True | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     def get_user_url(self, request: HttpRequest) -> HttpResponse: | ||||||
|  |         """Get a URL to a flow executor for either enrollment or authentication""" | ||||||
|  |         user_info, identifier = self.get_user_info() | ||||||
|  |         sfm = PlexSourceFlowManager( | ||||||
|  |             source=self._source, | ||||||
|  |             request=request, | ||||||
|  |             identifier=str(identifier), | ||||||
|  |             enroll_info=user_info, | ||||||
|  |         ) | ||||||
|  |         return sfm.get_flow(plex_token=self._token) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PlexSourceFlowManager(SourceFlowManager): | ||||||
|  |     """Flow manager for plex sources""" | ||||||
|  |  | ||||||
|  |     connection_type = PlexSourceConnection | ||||||
|  |  | ||||||
|  |     def update_connection( | ||||||
|  |         self, connection: PlexSourceConnection, plex_token: str | ||||||
|  |     ) -> PlexSourceConnection: | ||||||
|  |         """Set the access_token on the connection""" | ||||||
|  |         connection.plex_token = plex_token | ||||||
|  |         return connection | ||||||
							
								
								
									
										64
									
								
								authentik/sources/plex/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								authentik/sources/plex/tests.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | """plex Source tests""" | ||||||
|  | from django.test import TestCase | ||||||
|  | from requests_mock import Mocker | ||||||
|  |  | ||||||
|  | from authentik.providers.oauth2.generators import generate_client_secret | ||||||
|  | from authentik.sources.plex.models import PlexSource | ||||||
|  | from authentik.sources.plex.plex import PlexAuth | ||||||
|  |  | ||||||
|  | USER_INFO_RESPONSE = { | ||||||
|  |     "id": 1234123419, | ||||||
|  |     "uuid": "qwerqewrqewrqwr", | ||||||
|  |     "username": "username", | ||||||
|  |     "title": "title", | ||||||
|  |     "email": "foo@bar.baz", | ||||||
|  | } | ||||||
|  | RESOURCES_RESPONSE = [ | ||||||
|  |     { | ||||||
|  |         "name": "foo", | ||||||
|  |         "clientIdentifier": "allowed", | ||||||
|  |         "provides": "server", | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "name": "foo", | ||||||
|  |         "clientIdentifier": "denied", | ||||||
|  |         "provides": "server", | ||||||
|  |     }, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestPlexSource(TestCase): | ||||||
|  |     """plex Source tests""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         self.source: PlexSource = PlexSource.objects.create( | ||||||
|  |             name="test", | ||||||
|  |             slug="test", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_get_user_info(self): | ||||||
|  |         """Test get_user_info""" | ||||||
|  |         token = generate_client_secret() | ||||||
|  |         api = PlexAuth(self.source, token) | ||||||
|  |         with Mocker() as mocker: | ||||||
|  |             mocker.get("https://plex.tv/api/v2/user", json=USER_INFO_RESPONSE) | ||||||
|  |             self.assertEqual( | ||||||
|  |                 api.get_user_info(), | ||||||
|  |                 ( | ||||||
|  |                     {"username": "username", "email": "foo@bar.baz", "name": "title"}, | ||||||
|  |                     1234123419, | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     def test_check_server_overlap(self): | ||||||
|  |         """Test check_server_overlap""" | ||||||
|  |         token = generate_client_secret() | ||||||
|  |         api = PlexAuth(self.source, token) | ||||||
|  |         with Mocker() as mocker: | ||||||
|  |             mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE) | ||||||
|  |             self.assertFalse(api.check_server_overlap()) | ||||||
|  |         self.source.allowed_servers = ["allowed"] | ||||||
|  |         self.source.save() | ||||||
|  |         with Mocker() as mocker: | ||||||
|  |             mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE) | ||||||
|  |             self.assertTrue(api.check_server_overlap()) | ||||||
| @ -10,6 +10,7 @@ from rest_framework.serializers import Serializer | |||||||
| from authentik.core.models import Source | from authentik.core.models import Source | ||||||
| from authentik.core.types import UILoginButton | from authentik.core.types import UILoginButton | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
|  | from authentik.flows.challenge import ChallengeTypes, RedirectChallenge | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.utils.time import timedelta_string_validator | from authentik.lib.utils.time import timedelta_string_validator | ||||||
| from authentik.sources.saml.processors.constants import ( | from authentik.sources.saml.processors.constants import ( | ||||||
| @ -169,10 +170,16 @@ class SAMLSource(Source): | |||||||
|     @property |     @property | ||||||
|     def ui_login_button(self) -> UILoginButton: |     def ui_login_button(self) -> UILoginButton: | ||||||
|         return UILoginButton( |         return UILoginButton( | ||||||
|             name=self.name, |             challenge=RedirectChallenge( | ||||||
|             url=reverse( |                 instance={ | ||||||
|                 "authentik_sources_saml:login", kwargs={"source_slug": self.slug} |                     "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                     "to": reverse( | ||||||
|  |                         "authentik_sources_saml:login", | ||||||
|  |                         kwargs={"source_slug": self.slug}, | ||||||
|                     ), |                     ), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |             name=self.name, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|  | |||||||
| @ -115,7 +115,7 @@ class InitiateView(View): | |||||||
|             # Encode it back into a string |             # Encode it back into a string | ||||||
|             res = ParseResult( |             res = ParseResult( | ||||||
|                 scheme=sso_url.scheme, |                 scheme=sso_url.scheme, | ||||||
|                 netloc=sso_url.hostname or "", |                 netloc=sso_url.netloc, | ||||||
|                 path=sso_url.path, |                 path=sso_url.path, | ||||||
|                 params=sso_url.params, |                 params=sso_url.params, | ||||||
|                 query=urlencode(url_kwargs), |                 query=urlencode(url_kwargs), | ||||||
|  | |||||||
| @ -112,7 +112,9 @@ class IdentificationStageView(ChallengeStageView): | |||||||
|         for source in sources: |         for source in sources: | ||||||
|             ui_login_button = source.ui_login_button |             ui_login_button = source.ui_login_button | ||||||
|             if ui_login_button: |             if ui_login_button: | ||||||
|                 ui_sources.append(asdict(ui_login_button)) |                 button = asdict(ui_login_button) | ||||||
|  |                 button["challenge"] = ui_login_button.challenge.data | ||||||
|  |                 ui_sources.append(button) | ||||||
|         challenge.initial_data["sources"] = ui_sources |         challenge.initial_data["sources"] = ui_sources | ||||||
|         return challenge |         return challenge | ||||||
|  |  | ||||||
|  | |||||||
| @ -117,7 +117,10 @@ class TestIdentificationStage(TestCase): | |||||||
|                     { |                     { | ||||||
|                         "icon_url": "/static/authentik/sources/.svg", |                         "icon_url": "/static/authentik/sources/.svg", | ||||||
|                         "name": "test", |                         "name": "test", | ||||||
|                         "url": "/source/oauth/login/test/", |                         "challenge": { | ||||||
|  |                             "to": "/source/oauth/login/test/", | ||||||
|  |                             "type": "redirect", | ||||||
|  |                         }, | ||||||
|                     } |                     } | ||||||
|                 ], |                 ], | ||||||
|             }, |             }, | ||||||
| @ -158,9 +161,12 @@ class TestIdentificationStage(TestCase): | |||||||
|                 "title": self.flow.title, |                 "title": self.flow.title, | ||||||
|                 "sources": [ |                 "sources": [ | ||||||
|                     { |                     { | ||||||
|  |                         "challenge": { | ||||||
|  |                             "to": "/source/oauth/login/test/", | ||||||
|  |                             "type": "redirect", | ||||||
|  |                         }, | ||||||
|                         "icon_url": "/static/authentik/sources/.svg", |                         "icon_url": "/static/authentik/sources/.svg", | ||||||
|                         "name": "test", |                         "name": "test", | ||||||
|                         "url": "/source/oauth/login/test/", |  | ||||||
|                     } |                     } | ||||||
|                 ], |                 ], | ||||||
|             }, |             }, | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ class InvitationSerializer(ModelSerializer): | |||||||
|             "expires", |             "expires", | ||||||
|             "fixed_data", |             "fixed_data", | ||||||
|             "created_by", |             "created_by", | ||||||
|  |             "single_use", | ||||||
|         ] |         ] | ||||||
|         depth = 2 |         depth = 2 | ||||||
|  |  | ||||||
|  | |||||||
| @ -0,0 +1,21 @@ | |||||||
|  | # Generated by Django 3.2 on 2021-05-03 07:46 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_stages_invitation", "0003_auto_20201227_1210"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="invitation", | ||||||
|  |             name="single_use", | ||||||
|  |             field=models.BooleanField( | ||||||
|  |                 default=False, | ||||||
|  |                 help_text="When enabled, the invitation will be deleted after usage.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -53,6 +53,11 @@ class Invitation(models.Model): | |||||||
|  |  | ||||||
|     invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) |     invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |  | ||||||
|  |     single_use = models.BooleanField( | ||||||
|  |         default=False, | ||||||
|  |         help_text=_("When enabled, the invitation will be deleted after usage."), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     created_by = models.ForeignKey(User, on_delete=models.CASCADE) |     created_by = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|     expires = models.DateTimeField(default=None, blank=True, null=True) |     expires = models.DateTimeField(default=None, blank=True, null=True) | ||||||
|     fixed_data = models.JSONField( |     fixed_data = models.JSONField( | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """invitation stage logic""" | """invitation stage logic""" | ||||||
|  | from copy import deepcopy | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| @ -38,7 +39,9 @@ class InvitationStageView(StageView): | |||||||
|             return self.executor.stage_invalid() |             return self.executor.stage_invalid() | ||||||
|  |  | ||||||
|         invite: Invitation = get_object_or_404(Invitation, pk=token) |         invite: Invitation = get_object_or_404(Invitation, pk=token) | ||||||
|         self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data |         self.executor.plan.context[PLAN_CONTEXT_PROMPT] = deepcopy(invite.fixed_data) | ||||||
|         self.executor.plan.context[INVITATION_IN_EFFECT] = True |         self.executor.plan.context[INVITATION_IN_EFFECT] = True | ||||||
|         invitation_used.send(sender=self, request=request, invitation=invite) |         invitation_used.send(sender=self, request=request, invitation=invite) | ||||||
|  |         if invite.single_use: | ||||||
|  |             invite.delete() | ||||||
|         return self.executor.stage_ok() |         return self.executor.stage_ok() | ||||||
|  | |||||||
| @ -130,7 +130,7 @@ class TestUserLoginStage(TestCase): | |||||||
|         """Test with invitation, check data in session""" |         """Test with invitation, check data in session""" | ||||||
|         data = {"foo": "bar"} |         data = {"foo": "bar"} | ||||||
|         invite = Invitation.objects.create( |         invite = Invitation.objects.create( | ||||||
|             created_by=get_anonymous_user(), fixed_data=data |             created_by=get_anonymous_user(), fixed_data=data, single_use=True | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         plan = FlowPlan( |         plan = FlowPlan( | ||||||
| @ -156,6 +156,7 @@ class TestUserLoginStage(TestCase): | |||||||
|             force_str(response.content), |             force_str(response.content), | ||||||
|             {"to": reverse("authentik_core:root-redirect"), "type": "redirect"}, |             {"to": reverse("authentik_core:root-redirect"), "type": "redirect"}, | ||||||
|         ) |         ) | ||||||
|  |         self.assertFalse(Invitation.objects.filter(pk=invite.pk)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestInvitationsAPI(APITestCase): | class TestInvitationsAPI(APITestCase): | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ services: | |||||||
|       - .env |       - .env | ||||||
|   redis: |   redis: | ||||||
|     image: redis |     image: redis | ||||||
|  |     restart: unless-stopped | ||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|   server: |   server: | ||||||
|  | |||||||
							
								
								
									
										418
									
								
								swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										418
									
								
								swagger.yaml
									
									
									
									
									
								
							| @ -10213,6 +10213,238 @@ paths: | |||||||
|         description: A unique integer value identifying this User OAuth Source Connection. |         description: A unique integer value identifying this User OAuth Source Connection. | ||||||
|         required: true |         required: true | ||||||
|         type: integer |         type: integer | ||||||
|  |   /sources/plex/: | ||||||
|  |     get: | ||||||
|  |       operationId: sources_plex_list | ||||||
|  |       description: Plex source Viewset | ||||||
|  |       parameters: | ||||||
|  |         - name: ordering | ||||||
|  |           in: query | ||||||
|  |           description: Which field to use when ordering the results. | ||||||
|  |           required: false | ||||||
|  |           type: string | ||||||
|  |         - name: search | ||||||
|  |           in: query | ||||||
|  |           description: A search term. | ||||||
|  |           required: false | ||||||
|  |           type: string | ||||||
|  |         - name: page | ||||||
|  |           in: query | ||||||
|  |           description: Page Index | ||||||
|  |           required: false | ||||||
|  |           type: integer | ||||||
|  |         - name: page_size | ||||||
|  |           in: query | ||||||
|  |           description: Page Size | ||||||
|  |           required: false | ||||||
|  |           type: integer | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           description: '' | ||||||
|  |           schema: | ||||||
|  |             required: | ||||||
|  |               - results | ||||||
|  |               - pagination | ||||||
|  |             type: object | ||||||
|  |             properties: | ||||||
|  |               pagination: | ||||||
|  |                 required: | ||||||
|  |                   - next | ||||||
|  |                   - previous | ||||||
|  |                   - count | ||||||
|  |                   - current | ||||||
|  |                   - total_pages | ||||||
|  |                   - start_index | ||||||
|  |                   - end_index | ||||||
|  |                 type: object | ||||||
|  |                 properties: | ||||||
|  |                   next: | ||||||
|  |                     type: number | ||||||
|  |                   previous: | ||||||
|  |                     type: number | ||||||
|  |                   count: | ||||||
|  |                     type: number | ||||||
|  |                   current: | ||||||
|  |                     type: number | ||||||
|  |                   total_pages: | ||||||
|  |                     type: number | ||||||
|  |                   start_index: | ||||||
|  |                     type: number | ||||||
|  |                   end_index: | ||||||
|  |                     type: number | ||||||
|  |               results: | ||||||
|  |                 type: array | ||||||
|  |                 items: | ||||||
|  |                   $ref: '#/definitions/PlexSource' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     post: | ||||||
|  |       operationId: sources_plex_create | ||||||
|  |       description: Plex source Viewset | ||||||
|  |       parameters: | ||||||
|  |         - name: data | ||||||
|  |           in: body | ||||||
|  |           required: true | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |       responses: | ||||||
|  |         '201': | ||||||
|  |           description: '' | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |         '400': | ||||||
|  |           description: Invalid input. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/ValidationError' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     parameters: [] | ||||||
|  |   /sources/plex/redeem_token/: | ||||||
|  |     post: | ||||||
|  |       operationId: sources_plex_redeem_token | ||||||
|  |       description: |- | ||||||
|  |         Redeem a plex token, check it's access to resources against what's allowed | ||||||
|  |         for the source, and redirect to an authentication/enrollment flow. | ||||||
|  |       parameters: | ||||||
|  |         - name: data | ||||||
|  |           in: body | ||||||
|  |           required: true | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexTokenRedeem' | ||||||
|  |         - name: slug | ||||||
|  |           in: query | ||||||
|  |           type: string | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           description: '' | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/RedirectChallenge' | ||||||
|  |         '404': | ||||||
|  |           description: Token not found | ||||||
|  |         '400': | ||||||
|  |           description: Invalid input. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/ValidationError' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     parameters: [] | ||||||
|  |   /sources/plex/{slug}/: | ||||||
|  |     get: | ||||||
|  |       operationId: sources_plex_read | ||||||
|  |       description: Plex source Viewset | ||||||
|  |       parameters: [] | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           description: '' | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |         '404': | ||||||
|  |           description: Object does not exist or caller has insufficient permissions | ||||||
|  |             to access it. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/APIException' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     put: | ||||||
|  |       operationId: sources_plex_update | ||||||
|  |       description: Plex source Viewset | ||||||
|  |       parameters: | ||||||
|  |         - name: data | ||||||
|  |           in: body | ||||||
|  |           required: true | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           description: '' | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |         '400': | ||||||
|  |           description: Invalid input. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/ValidationError' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |         '404': | ||||||
|  |           description: Object does not exist or caller has insufficient permissions | ||||||
|  |             to access it. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/APIException' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     patch: | ||||||
|  |       operationId: sources_plex_partial_update | ||||||
|  |       description: Plex source Viewset | ||||||
|  |       parameters: | ||||||
|  |         - name: data | ||||||
|  |           in: body | ||||||
|  |           required: true | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |       responses: | ||||||
|  |         '200': | ||||||
|  |           description: '' | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/PlexSource' | ||||||
|  |         '400': | ||||||
|  |           description: Invalid input. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/ValidationError' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |         '404': | ||||||
|  |           description: Object does not exist or caller has insufficient permissions | ||||||
|  |             to access it. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/APIException' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     delete: | ||||||
|  |       operationId: sources_plex_delete | ||||||
|  |       description: Plex source Viewset | ||||||
|  |       parameters: [] | ||||||
|  |       responses: | ||||||
|  |         '204': | ||||||
|  |           description: '' | ||||||
|  |         '403': | ||||||
|  |           description: Authentication credentials were invalid, absent or insufficient. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/GenericError' | ||||||
|  |         '404': | ||||||
|  |           description: Object does not exist or caller has insufficient permissions | ||||||
|  |             to access it. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/APIException' | ||||||
|  |       tags: | ||||||
|  |         - sources | ||||||
|  |     parameters: | ||||||
|  |       - name: slug | ||||||
|  |         in: path | ||||||
|  |         description: Internal source name, used in URLs. | ||||||
|  |         required: true | ||||||
|  |         type: string | ||||||
|  |         format: slug | ||||||
|  |         pattern: ^[-a-zA-Z0-9_]+$ | ||||||
|   /sources/saml/: |   /sources/saml/: | ||||||
|     get: |     get: | ||||||
|       operationId: sources_saml_list |       operationId: sources_saml_list | ||||||
| @ -16210,6 +16442,7 @@ definitions: | |||||||
|           - authentik.recovery |           - authentik.recovery | ||||||
|           - authentik.sources.ldap |           - authentik.sources.ldap | ||||||
|           - authentik.sources.oauth |           - authentik.sources.oauth | ||||||
|  |           - authentik.sources.plex | ||||||
|           - authentik.sources.saml |           - authentik.sources.saml | ||||||
|           - authentik.stages.authenticator_static |           - authentik.stages.authenticator_static | ||||||
|           - authentik.stages.authenticator_totp |           - authentik.stages.authenticator_totp | ||||||
| @ -17056,6 +17289,17 @@ definitions: | |||||||
|         enum: |         enum: | ||||||
|           - all |           - all | ||||||
|           - any |           - any | ||||||
|  |       user_matching_mode: | ||||||
|  |         title: User matching mode | ||||||
|  |         description: How the source determines if an existing user should be authenticated | ||||||
|  |           or a new user enrolled. | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - identifier | ||||||
|  |           - email_link | ||||||
|  |           - email_deny | ||||||
|  |           - username_link | ||||||
|  |           - username_deny | ||||||
|   UserSetting: |   UserSetting: | ||||||
|     required: |     required: | ||||||
|       - object_uid |       - object_uid | ||||||
| @ -17136,6 +17380,17 @@ definitions: | |||||||
|         enum: |         enum: | ||||||
|           - all |           - all | ||||||
|           - any |           - any | ||||||
|  |       user_matching_mode: | ||||||
|  |         title: User matching mode | ||||||
|  |         description: How the source determines if an existing user should be authenticated | ||||||
|  |           or a new user enrolled. | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - identifier | ||||||
|  |           - email_link | ||||||
|  |           - email_deny | ||||||
|  |           - username_link | ||||||
|  |           - username_deny | ||||||
|       server_uri: |       server_uri: | ||||||
|         title: Server URI |         title: Server URI | ||||||
|         type: string |         type: string | ||||||
| @ -17316,6 +17571,17 @@ definitions: | |||||||
|         enum: |         enum: | ||||||
|           - all |           - all | ||||||
|           - any |           - any | ||||||
|  |       user_matching_mode: | ||||||
|  |         title: User matching mode | ||||||
|  |         description: How the source determines if an existing user should be authenticated | ||||||
|  |           or a new user enrolled. | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - identifier | ||||||
|  |           - email_link | ||||||
|  |           - email_deny | ||||||
|  |           - username_link | ||||||
|  |           - username_deny | ||||||
|       provider_type: |       provider_type: | ||||||
|         title: Provider type |         title: Provider type | ||||||
|         type: string |         type: string | ||||||
| @ -17386,6 +17652,132 @@ definitions: | |||||||
|         type: string |         type: string | ||||||
|         maxLength: 255 |         maxLength: 255 | ||||||
|         minLength: 1 |         minLength: 1 | ||||||
|  |   PlexSource: | ||||||
|  |     required: | ||||||
|  |       - name | ||||||
|  |       - slug | ||||||
|  |     type: object | ||||||
|  |     properties: | ||||||
|  |       pk: | ||||||
|  |         title: Pbm uuid | ||||||
|  |         type: string | ||||||
|  |         format: uuid | ||||||
|  |         readOnly: true | ||||||
|  |       name: | ||||||
|  |         title: Name | ||||||
|  |         description: Source's display Name. | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|  |       slug: | ||||||
|  |         title: Slug | ||||||
|  |         description: Internal source name, used in URLs. | ||||||
|  |         type: string | ||||||
|  |         format: slug | ||||||
|  |         pattern: ^[-a-zA-Z0-9_]+$ | ||||||
|  |         maxLength: 50 | ||||||
|  |         minLength: 1 | ||||||
|  |       enabled: | ||||||
|  |         title: Enabled | ||||||
|  |         type: boolean | ||||||
|  |       authentication_flow: | ||||||
|  |         title: Authentication flow | ||||||
|  |         description: Flow to use when authenticating existing users. | ||||||
|  |         type: string | ||||||
|  |         format: uuid | ||||||
|  |         x-nullable: true | ||||||
|  |       enrollment_flow: | ||||||
|  |         title: Enrollment flow | ||||||
|  |         description: Flow to use when enrolling new users. | ||||||
|  |         type: string | ||||||
|  |         format: uuid | ||||||
|  |         x-nullable: true | ||||||
|  |       component: | ||||||
|  |         title: Component | ||||||
|  |         type: string | ||||||
|  |         readOnly: true | ||||||
|  |       verbose_name: | ||||||
|  |         title: Verbose name | ||||||
|  |         type: string | ||||||
|  |         readOnly: true | ||||||
|  |       verbose_name_plural: | ||||||
|  |         title: Verbose name plural | ||||||
|  |         type: string | ||||||
|  |         readOnly: true | ||||||
|  |       policy_engine_mode: | ||||||
|  |         title: Policy engine mode | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - all | ||||||
|  |           - any | ||||||
|  |       user_matching_mode: | ||||||
|  |         title: User matching mode | ||||||
|  |         description: How the source determines if an existing user should be authenticated | ||||||
|  |           or a new user enrolled. | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - identifier | ||||||
|  |           - email_link | ||||||
|  |           - email_deny | ||||||
|  |           - username_link | ||||||
|  |           - username_deny | ||||||
|  |       client_id: | ||||||
|  |         title: Client id | ||||||
|  |         description: Client identifier used to talk to Plex. | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|  |       allowed_servers: | ||||||
|  |         description: Which servers a user has to be a member of to be granted access. | ||||||
|  |           Empty list allows every server. | ||||||
|  |         type: array | ||||||
|  |         items: | ||||||
|  |           title: Allowed servers | ||||||
|  |           type: string | ||||||
|  |           minLength: 1 | ||||||
|  |   PlexTokenRedeem: | ||||||
|  |     required: | ||||||
|  |       - plex_token | ||||||
|  |     type: object | ||||||
|  |     properties: | ||||||
|  |       plex_token: | ||||||
|  |         title: Plex token | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|  |   RedirectChallenge: | ||||||
|  |     required: | ||||||
|  |       - type | ||||||
|  |       - to | ||||||
|  |     type: object | ||||||
|  |     properties: | ||||||
|  |       type: | ||||||
|  |         title: Type | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - native | ||||||
|  |           - shell | ||||||
|  |           - redirect | ||||||
|  |       component: | ||||||
|  |         title: Component | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|  |       title: | ||||||
|  |         title: Title | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|  |       background: | ||||||
|  |         title: Background | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|  |       response_errors: | ||||||
|  |         title: Response errors | ||||||
|  |         type: object | ||||||
|  |         additionalProperties: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             $ref: '#/definitions/ErrorDetail' | ||||||
|  |       to: | ||||||
|  |         title: To | ||||||
|  |         type: string | ||||||
|  |         minLength: 1 | ||||||
|   SAMLSource: |   SAMLSource: | ||||||
|     required: |     required: | ||||||
|       - name |       - name | ||||||
| @ -17445,6 +17837,17 @@ definitions: | |||||||
|         enum: |         enum: | ||||||
|           - all |           - all | ||||||
|           - any |           - any | ||||||
|  |       user_matching_mode: | ||||||
|  |         title: User matching mode | ||||||
|  |         description: How the source determines if an existing user should be authenticated | ||||||
|  |           or a new user enrolled. | ||||||
|  |         type: string | ||||||
|  |         enum: | ||||||
|  |           - identifier | ||||||
|  |           - email_link | ||||||
|  |           - email_deny | ||||||
|  |           - username_link | ||||||
|  |           - username_deny | ||||||
|       pre_authentication_flow: |       pre_authentication_flow: | ||||||
|         title: Pre authentication flow |         title: Pre authentication flow | ||||||
|         description: Flow used before authentication. |         description: Flow used before authentication. | ||||||
| @ -18190,6 +18593,17 @@ definitions: | |||||||
|                 enabled: |                 enabled: | ||||||
|                   title: Enabled |                   title: Enabled | ||||||
|                   type: boolean |                   type: boolean | ||||||
|  |                 user_matching_mode: | ||||||
|  |                   title: User matching mode | ||||||
|  |                   description: How the source determines if an existing user should | ||||||
|  |                     be authenticated or a new user enrolled. | ||||||
|  |                   type: string | ||||||
|  |                   enum: | ||||||
|  |                     - identifier | ||||||
|  |                     - email_link | ||||||
|  |                     - email_deny | ||||||
|  |                     - username_link | ||||||
|  |                     - username_deny | ||||||
|                 authentication_flow: |                 authentication_flow: | ||||||
|                   title: Authentication flow |                   title: Authentication flow | ||||||
|                   description: Flow to use when authenticating existing users. |                   description: Flow to use when authenticating existing users. | ||||||
| @ -18248,6 +18662,10 @@ definitions: | |||||||
|                   x-nullable: true |                   x-nullable: true | ||||||
|             readOnly: true |             readOnly: true | ||||||
|         readOnly: true |         readOnly: true | ||||||
|  |       single_use: | ||||||
|  |         title: Single use | ||||||
|  |         description: When enabled, the invitation will be deleted after usage. | ||||||
|  |         type: boolean | ||||||
|   InvitationStage: |   InvitationStage: | ||||||
|     required: |     required: | ||||||
|       - name |       - name | ||||||
|  | |||||||
| @ -147,11 +147,11 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         # Now we should be at the IDP, wait for the login field |         # Now we should be at the IDP, wait for the login field | ||||||
| @ -206,11 +206,11 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         # Now we should be at the IDP, wait for the login field |         # Now we should be at the IDP, wait for the login field | ||||||
| @ -245,11 +245,11 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         # Now we should be at the IDP, wait for the login field |         # Now we should be at the IDP, wait for the login field | ||||||
| @ -338,17 +338,18 @@ class TestSourceOAuth1(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         # Now we should be at the IDP, wait for the login field |         # Now we should be at the IDP, wait for the login field | ||||||
|         self.wait.until(ec.presence_of_element_located((By.NAME, "username"))) |         self.wait.until(ec.presence_of_element_located((By.NAME, "username"))) | ||||||
|         self.driver.find_element(By.NAME, "username").send_keys("example-user") |         self.driver.find_element(By.NAME, "username").send_keys("example-user") | ||||||
|         self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER) |         self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER) | ||||||
|  |         sleep(2) | ||||||
|  |  | ||||||
|         # Wait until we're logged in |         # Wait until we're logged in | ||||||
|         self.wait.until( |         self.wait.until( | ||||||
|  | |||||||
| @ -140,11 +140,11 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         # Now we should be at the IDP, wait for the username field |         # Now we should be at the IDP, wait for the username field | ||||||
| @ -208,11 +208,11 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|         sleep(1) |         sleep(1) | ||||||
|  |  | ||||||
| @ -289,11 +289,11 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|  |  | ||||||
|         wait.until( |         wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
|                 (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") |                 (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button") | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         identification_stage.find_element( |         identification_stage.find_element( | ||||||
|             By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" |             By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         # Now we should be at the IDP, wait for the username field |         # Now we should be at the IDP, wait for the username field | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ from authentik.core.api.users import UserSerializer | |||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.managed.manager import ObjectManager | from authentik.managed.manager import ObjectManager | ||||||
|  |  | ||||||
| RETRIES = int(environ.get("RETRIES", "3")) | RETRIES = int(environ.get("RETRIES", "5")) | ||||||
|  |  | ||||||
| # pylint: disable=invalid-name | # pylint: disable=invalid-name | ||||||
| def USER() -> User:  # noqa | def USER() -> User:  # noqa | ||||||
|  | |||||||
| @ -6,7 +6,8 @@ | |||||||
|     "extends": [ |     "extends": [ | ||||||
|         "eslint:recommended", |         "eslint:recommended", | ||||||
|         "plugin:@typescript-eslint/recommended", |         "plugin:@typescript-eslint/recommended", | ||||||
|         "plugin:lit/recommended" |         "plugin:lit/recommended", | ||||||
|  |         "plugin:custom-elements/recommended" | ||||||
|     ], |     ], | ||||||
|     "parser": "@typescript-eslint/parser", |     "parser": "@typescript-eslint/parser", | ||||||
|     "parserOptions": { |     "parserOptions": { | ||||||
| @ -15,7 +16,8 @@ | |||||||
|     }, |     }, | ||||||
|     "plugins": [ |     "plugins": [ | ||||||
|         "@typescript-eslint", |         "@typescript-eslint", | ||||||
|         "lit" |         "lit", | ||||||
|  |         "custom-elements" | ||||||
|     ], |     ], | ||||||
|     "rules": { |     "rules": { | ||||||
|         "indent": "off", |         "indent": "off", | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ stages: | |||||||
|         steps: |         steps: | ||||||
|           - task: NodeTool@0 |           - task: NodeTool@0 | ||||||
|             inputs: |             inputs: | ||||||
|               versionSpec: '12.x' |               versionSpec: '14.x' | ||||||
|             displayName: 'Install Node.js' |             displayName: 'Install Node.js' | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             inputs: |             inputs: | ||||||
| @ -31,7 +31,7 @@ stages: | |||||||
|         steps: |         steps: | ||||||
|           - task: NodeTool@0 |           - task: NodeTool@0 | ||||||
|             inputs: |             inputs: | ||||||
|               versionSpec: '12.x' |               versionSpec: '14.x' | ||||||
|             displayName: 'Install Node.js' |             displayName: 'Install Node.js' | ||||||
|           - task: DownloadPipelineArtifact@2 |           - task: DownloadPipelineArtifact@2 | ||||||
|             inputs: |             inputs: | ||||||
| @ -53,7 +53,7 @@ stages: | |||||||
|         steps: |         steps: | ||||||
|           - task: NodeTool@0 |           - task: NodeTool@0 | ||||||
|             inputs: |             inputs: | ||||||
|               versionSpec: '12.x' |               versionSpec: '14.x' | ||||||
|             displayName: 'Install Node.js' |             displayName: 'Install Node.js' | ||||||
|           - task: DownloadPipelineArtifact@2 |           - task: DownloadPipelineArtifact@2 | ||||||
|             inputs: |             inputs: | ||||||
| @ -77,7 +77,7 @@ stages: | |||||||
|         steps: |         steps: | ||||||
|           - task: NodeTool@0 |           - task: NodeTool@0 | ||||||
|             inputs: |             inputs: | ||||||
|               versionSpec: '12.x' |               versionSpec: '14.x' | ||||||
|             displayName: 'Install Node.js' |             displayName: 'Install Node.js' | ||||||
|           - task: DownloadPipelineArtifact@2 |           - task: DownloadPipelineArtifact@2 | ||||||
|             inputs: |             inputs: | ||||||
|  | |||||||
							
								
								
									
										155
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										155
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -2302,27 +2302,27 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "@sentry/browser": { |         "@sentry/browser": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.3.5.tgz", | ||||||
|             "integrity": "sha512-AXqHK5aeMKJPc4zf4lLBlj9TOxzSAmht4Zk0TxXWCsJ6AFP2H/nq20przQJkFyc7m8Ob8VhiNkeA7BQsMyiX6g==", |             "integrity": "sha512-fjkhPR5gLCGVWhbWjEoN64hnmTvfTLRCgWmYTc9SiGchWFoFEmLqZyF2uJFyt27+qamLQ9fN58nnv4Ly2yyxqg==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "@sentry/core": "6.3.4", |                 "@sentry/core": "6.3.5", | ||||||
|                 "@sentry/types": "6.3.4", |                 "@sentry/types": "6.3.5", | ||||||
|                 "@sentry/utils": "6.3.4", |                 "@sentry/utils": "6.3.5", | ||||||
|                 "tslib": "^1.9.3" |                 "tslib": "^1.9.3" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry/types": { |                 "@sentry/types": { | ||||||
|                     "version": "6.3.4", |                     "version": "6.3.5", | ||||||
|                     "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.4.tgz", |                     "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", | ||||||
|                     "integrity": "sha512-z1tCcBE9HDxDJq9ehfaNeyNJn5RXDNfC6eO8xB5JsJmUwbqTMCuInMWL566y2zS2kVpskZOsj4mj5/FRGRHw2g==" |                     "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" | ||||||
|                 }, |                 }, | ||||||
|                 "@sentry/utils": { |                 "@sentry/utils": { | ||||||
|                     "version": "6.3.4", |                     "version": "6.3.5", | ||||||
|                     "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.4.tgz", |                     "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz", | ||||||
|                     "integrity": "sha512-QlN+PZc3GqfiCGP+kP5c9yyGXTze9+hOkmKprXlSOJqHIOfIfN3Crh7JPdRMhAW+Taj1xKQPO+BQ1cj61aoIoQ==", |                     "integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==", | ||||||
|                     "requires": { |                     "requires": { | ||||||
|                         "@sentry/types": "6.3.4", |                         "@sentry/types": "6.3.5", | ||||||
|                         "tslib": "^1.9.3" |                         "tslib": "^1.9.3" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
| @ -2334,48 +2334,48 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "@sentry/core": { |         "@sentry/core": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.3.5.tgz", | ||||||
|             "integrity": "sha512-M1C09EFpRDYDU968dk4rDIciX7v4q2eewS9kBPGwdWLbuSIO9yhsvEw3bK1XqatQSxnfQoXsO33ADq/JdWnGUA==", |             "integrity": "sha512-VR2ibDy33mryD0mT6d9fGhKjdNzS2FSwwZPe9GvmNOjkyjly/oV91BKVoYJneCqOeq8fyj2lvkJGKuupdJNDqg==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "@sentry/hub": "6.3.4", |                 "@sentry/hub": "6.3.5", | ||||||
|                 "@sentry/minimal": "6.3.4", |                 "@sentry/minimal": "6.3.5", | ||||||
|                 "@sentry/types": "6.3.4", |                 "@sentry/types": "6.3.5", | ||||||
|                 "@sentry/utils": "6.3.4", |                 "@sentry/utils": "6.3.5", | ||||||
|                 "tslib": "^1.9.3" |                 "tslib": "^1.9.3" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@sentry/hub": { |                 "@sentry/hub": { | ||||||
|                     "version": "6.3.4", |                     "version": "6.3.5", | ||||||
|                     "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.4.tgz", |                     "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.5.tgz", | ||||||
|                     "integrity": "sha512-G9JVP851ovzkOnNzzm9s+g6Fkl+2ulfgsdjE8afd6/y0xUliCScdUCyU408AxnSyKTmBb8Cx7J+FDiqM+HQeaA==", |                     "integrity": "sha512-ZYFo7VYKwdPVjuV9BDFiYn+MpANn6eZMz5QDBfZ2dugIvIVbuOyOOLx8PSa3ZXJoVTZZ7s2wD2fi/ZxKjNjZOQ==", | ||||||
|                     "requires": { |                     "requires": { | ||||||
|                         "@sentry/types": "6.3.4", |                         "@sentry/types": "6.3.5", | ||||||
|                         "@sentry/utils": "6.3.4", |                         "@sentry/utils": "6.3.5", | ||||||
|                         "tslib": "^1.9.3" |                         "tslib": "^1.9.3" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "@sentry/minimal": { |                 "@sentry/minimal": { | ||||||
|                     "version": "6.3.4", |                     "version": "6.3.5", | ||||||
|                     "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.4.tgz", |                     "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.5.tgz", | ||||||
|                     "integrity": "sha512-amtQXu6jQmBjBJJTyvzsvMWFmwP3kfdkj2LVfNA40qInr6IJ200jQrZ/KUKngScK0kPfNDW4qFTE/U4J60m22Q==", |                     "integrity": "sha512-4RqIGAU0+8iI/1sw0GYPTr4SUA88/i2+JPjFJ+qloh5ANVaNwhFPRChw+Ys9xpre8LV9JZrEsEf8AvQr4fkNbA==", | ||||||
|                     "requires": { |                     "requires": { | ||||||
|                         "@sentry/hub": "6.3.4", |                         "@sentry/hub": "6.3.5", | ||||||
|                         "@sentry/types": "6.3.4", |                         "@sentry/types": "6.3.5", | ||||||
|                         "tslib": "^1.9.3" |                         "tslib": "^1.9.3" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 "@sentry/types": { |                 "@sentry/types": { | ||||||
|                     "version": "6.3.4", |                     "version": "6.3.5", | ||||||
|                     "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.4.tgz", |                     "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", | ||||||
|                     "integrity": "sha512-z1tCcBE9HDxDJq9ehfaNeyNJn5RXDNfC6eO8xB5JsJmUwbqTMCuInMWL566y2zS2kVpskZOsj4mj5/FRGRHw2g==" |                     "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" | ||||||
|                 }, |                 }, | ||||||
|                 "@sentry/utils": { |                 "@sentry/utils": { | ||||||
|                     "version": "6.3.4", |                     "version": "6.3.5", | ||||||
|                     "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.4.tgz", |                     "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz", | ||||||
|                     "integrity": "sha512-QlN+PZc3GqfiCGP+kP5c9yyGXTze9+hOkmKprXlSOJqHIOfIfN3Crh7JPdRMhAW+Taj1xKQPO+BQ1cj61aoIoQ==", |                     "integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==", | ||||||
|                     "requires": { |                     "requires": { | ||||||
|                         "@sentry/types": "6.3.4", |                         "@sentry/types": "6.3.5", | ||||||
|                         "tslib": "^1.9.3" |                         "tslib": "^1.9.3" | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
| @ -2387,12 +2387,12 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "@sentry/hub": { |         "@sentry/hub": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.3.5.tgz", | ||||||
|             "integrity": "sha512-G9JVP851ovzkOnNzzm9s+g6Fkl+2ulfgsdjE8afd6/y0xUliCScdUCyU408AxnSyKTmBb8Cx7J+FDiqM+HQeaA==", |             "integrity": "sha512-ZYFo7VYKwdPVjuV9BDFiYn+MpANn6eZMz5QDBfZ2dugIvIVbuOyOOLx8PSa3ZXJoVTZZ7s2wD2fi/ZxKjNjZOQ==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "@sentry/types": "6.3.4", |                 "@sentry/types": "6.3.5", | ||||||
|                 "@sentry/utils": "6.3.4", |                 "@sentry/utils": "6.3.5", | ||||||
|                 "tslib": "^1.9.3" |                 "tslib": "^1.9.3" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
| @ -2404,12 +2404,12 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "@sentry/minimal": { |         "@sentry/minimal": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.3.5.tgz", | ||||||
|             "integrity": "sha512-amtQXu6jQmBjBJJTyvzsvMWFmwP3kfdkj2LVfNA40qInr6IJ200jQrZ/KUKngScK0kPfNDW4qFTE/U4J60m22Q==", |             "integrity": "sha512-4RqIGAU0+8iI/1sw0GYPTr4SUA88/i2+JPjFJ+qloh5ANVaNwhFPRChw+Ys9xpre8LV9JZrEsEf8AvQr4fkNbA==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "@sentry/hub": "6.3.4", |                 "@sentry/hub": "6.3.5", | ||||||
|                 "@sentry/types": "6.3.4", |                 "@sentry/types": "6.3.5", | ||||||
|                 "tslib": "^1.9.3" |                 "tslib": "^1.9.3" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
| @ -2421,14 +2421,14 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "@sentry/tracing": { |         "@sentry/tracing": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.3.5.tgz", | ||||||
|             "integrity": "sha512-CpjIfVpi/u/Uraz1mUsteytovn47aGLWltAFrpn7bew/Y0hqnULGx/D/FwtQ4EbcdgULNtOX+nTrxJ5abmwZ3w==", |             "integrity": "sha512-TNKAST1ge2g24BlTfVxNp4gP5t3drbi0OVCh8h8ah+J7UjHSfdiqhd9W2h5qv1GO61gGlpWeN/TyioyQmOxu0Q==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "@sentry/hub": "6.3.4", |                 "@sentry/hub": "6.3.5", | ||||||
|                 "@sentry/minimal": "6.3.4", |                 "@sentry/minimal": "6.3.5", | ||||||
|                 "@sentry/types": "6.3.4", |                 "@sentry/types": "6.3.5", | ||||||
|                 "@sentry/utils": "6.3.4", |                 "@sentry/utils": "6.3.5", | ||||||
|                 "tslib": "^1.9.3" |                 "tslib": "^1.9.3" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
| @ -2440,16 +2440,16 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "@sentry/types": { |         "@sentry/types": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.3.5.tgz", | ||||||
|             "integrity": "sha512-z1tCcBE9HDxDJq9ehfaNeyNJn5RXDNfC6eO8xB5JsJmUwbqTMCuInMWL566y2zS2kVpskZOsj4mj5/FRGRHw2g==" |             "integrity": "sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ==" | ||||||
|         }, |         }, | ||||||
|         "@sentry/utils": { |         "@sentry/utils": { | ||||||
|             "version": "6.3.4", |             "version": "6.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.4.tgz", |             "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.3.5.tgz", | ||||||
|             "integrity": "sha512-QlN+PZc3GqfiCGP+kP5c9yyGXTze9+hOkmKprXlSOJqHIOfIfN3Crh7JPdRMhAW+Taj1xKQPO+BQ1cj61aoIoQ==", |             "integrity": "sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "@sentry/types": "6.3.4", |                 "@sentry/types": "6.3.5", | ||||||
|                 "tslib": "^1.9.3" |                 "tslib": "^1.9.3" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
| @ -3174,9 +3174,9 @@ | |||||||
|             "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" |             "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" | ||||||
|         }, |         }, | ||||||
|         "chart.js": { |         "chart.js": { | ||||||
|             "version": "3.2.0", |             "version": "3.2.1", | ||||||
|             "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.2.0.tgz", |             "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.2.1.tgz", | ||||||
|             "integrity": "sha512-Ml3R47TvOPW6gQ6T8mg/uPvyOASPpPVVF6xb7ZyHkek1c6kJIT5ScT559afXoDf6uwtpDR2BpCommkA5KT8ODg==" |             "integrity": "sha512-XsNDf3854RGZkLCt+5vWAXGAtUdKP2nhfikLGZqud6G4CvRE2ts64TIxTTfspOin2kEZvPgomE29E6oU02dYjQ==" | ||||||
|         }, |         }, | ||||||
|         "chartjs-adapter-moment": { |         "chartjs-adapter-moment": { | ||||||
|             "version": "1.0.0", |             "version": "1.0.0", | ||||||
| @ -3723,6 +3723,14 @@ | |||||||
|             "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", | ||||||
|             "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==" |             "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==" | ||||||
|         }, |         }, | ||||||
|  |         "eslint-plugin-custom-elements": { | ||||||
|  |             "version": "0.0.2", | ||||||
|  |             "resolved": "https://registry.npmjs.org/eslint-plugin-custom-elements/-/eslint-plugin-custom-elements-0.0.2.tgz", | ||||||
|  |             "integrity": "sha512-lIRBhxh0M/1seyMzSPJwdfdNtlVSPArJ+erF2xqjPsd/6SdCuT43hCQNV2A2te3GqBWhgh/unXSVRO09c1kyPA==", | ||||||
|  |             "requires": { | ||||||
|  |                 "eslint-rule-documentation": ">=1.0.0" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "eslint-plugin-lit": { |         "eslint-plugin-lit": { | ||||||
|             "version": "1.3.0", |             "version": "1.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.3.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.3.0.tgz", | ||||||
| @ -3733,6 +3741,11 @@ | |||||||
|                 "requireindex": "^1.2.0" |                 "requireindex": "^1.2.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "eslint-rule-documentation": { | ||||||
|  |             "version": "1.0.23", | ||||||
|  |             "resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz", | ||||||
|  |             "integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==" | ||||||
|  |         }, | ||||||
|         "eslint-scope": { |         "eslint-scope": { | ||||||
|             "version": "5.1.1", |             "version": "5.1.1", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", |             "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", | ||||||
| @ -4924,9 +4937,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "lit-element": { |         "lit-element": { | ||||||
|             "version": "2.4.0", |             "version": "2.5.0", | ||||||
|             "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.4.0.tgz", |             "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.0.tgz", | ||||||
|             "integrity": "sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==", |             "integrity": "sha512-SS6Bmm7FYw/RVeD6YD3gAjrT0ss6rOQHaacUnDCyVE3sDuUpEmr+Gjl0QUHnD8+0mM5apBbnA60NkFJ2kqcOMA==", | ||||||
|             "requires": { |             "requires": { | ||||||
|                 "lit-html": "^1.1.1" |                 "lit-html": "^1.1.1" | ||||||
|             } |             } | ||||||
| @ -5693,6 +5706,14 @@ | |||||||
|                 "prismjs": "^1.23.0" |                 "prismjs": "^1.23.0" | ||||||
|             }, |             }, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  |                 "lit-element": { | ||||||
|  |                     "version": "2.4.0", | ||||||
|  |                     "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.4.0.tgz", | ||||||
|  |                     "integrity": "sha512-pBGLglxyhq/Prk2H91nA0KByq/hx/wssJBQFiYqXhGDvEnY31PRGYf1RglVzyLeRysu0IHm2K0P196uLLWmwFg==", | ||||||
|  |                     "requires": { | ||||||
|  |                         "lit-html": "^1.1.1" | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|                 "lit-html": { |                 "lit-html": { | ||||||
|                     "version": "1.2.1", |                     "version": "1.2.1", | ||||||
|                     "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.2.1.tgz", |                     "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.2.1.tgz", | ||||||
|  | |||||||
| @ -50,8 +50,8 @@ | |||||||
|         "@rollup/plugin-babel": "^5.3.0", |         "@rollup/plugin-babel": "^5.3.0", | ||||||
|         "@rollup/plugin-replace": "^2.4.2", |         "@rollup/plugin-replace": "^2.4.2", | ||||||
|         "@rollup/plugin-typescript": "^8.2.1", |         "@rollup/plugin-typescript": "^8.2.1", | ||||||
|         "@sentry/browser": "^6.3.4", |         "@sentry/browser": "^6.3.5", | ||||||
|         "@sentry/tracing": "^6.3.4", |         "@sentry/tracing": "^6.3.5", | ||||||
|         "@types/chart.js": "^2.9.32", |         "@types/chart.js": "^2.9.32", | ||||||
|         "@types/codemirror": "0.0.109", |         "@types/codemirror": "0.0.109", | ||||||
|         "@types/grecaptcha": "^3.0.2", |         "@types/grecaptcha": "^3.0.2", | ||||||
| @ -61,15 +61,16 @@ | |||||||
|         "authentik-api": "file:api", |         "authentik-api": "file:api", | ||||||
|         "babel-plugin-macros": "^3.0.1", |         "babel-plugin-macros": "^3.0.1", | ||||||
|         "base64-js": "^1.5.1", |         "base64-js": "^1.5.1", | ||||||
|         "chart.js": "^3.2.0", |         "chart.js": "^3.2.1", | ||||||
|         "chartjs-adapter-moment": "^1.0.0", |         "chartjs-adapter-moment": "^1.0.0", | ||||||
|         "codemirror": "^5.61.0", |         "codemirror": "^5.61.0", | ||||||
|         "construct-style-sheets-polyfill": "^2.4.16", |         "construct-style-sheets-polyfill": "^2.4.16", | ||||||
|         "eslint": "^7.25.0", |         "eslint": "^7.25.0", | ||||||
|         "eslint-config-google": "^0.14.0", |         "eslint-config-google": "^0.14.0", | ||||||
|  |         "eslint-plugin-custom-elements": "0.0.2", | ||||||
|         "eslint-plugin-lit": "^1.3.0", |         "eslint-plugin-lit": "^1.3.0", | ||||||
|         "flowchart.js": "^1.15.0", |         "flowchart.js": "^1.15.0", | ||||||
|         "lit-element": "^2.4.0", |         "lit-element": "^2.5.0", | ||||||
|         "lit-html": "^1.4.0", |         "lit-html": "^1.4.0", | ||||||
|         "moment": "^2.29.1", |         "moment": "^2.29.1", | ||||||
|         "rapidoc": "^9.0.0", |         "rapidoc": "^9.0.0", | ||||||
|  | |||||||
| @ -272,7 +272,7 @@ body { | |||||||
|     .pf-c-login__main-header-desc { |     .pf-c-login__main-header-desc { | ||||||
|         color: var(--ak-dark-foreground); |         color: var(--ak-dark-foreground); | ||||||
|     } |     } | ||||||
|     .pf-c-login__main-footer-links-item-link > img { |     .pf-c-login__main-footer-links-item img { | ||||||
|         filter: invert(1); |         filter: invert(1); | ||||||
|     } |     } | ||||||
|     .pf-c-login__main-footer-band { |     .pf-c-login__main-footer-band { | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ import "./stages/email/EmailStage"; | |||||||
| import "./stages/identification/IdentificationStage"; | import "./stages/identification/IdentificationStage"; | ||||||
| import "./stages/password/PasswordStage"; | import "./stages/password/PasswordStage"; | ||||||
| import "./stages/prompt/PromptStage"; | import "./stages/prompt/PromptStage"; | ||||||
|  | import "./sources/plex/PlexLoginInit"; | ||||||
| import { ShellChallenge, RedirectChallenge } from "../api/Flows"; | import { ShellChallenge, RedirectChallenge } from "../api/Flows"; | ||||||
| import { IdentificationChallenge } from "./stages/identification/IdentificationStage"; | import { IdentificationChallenge } from "./stages/identification/IdentificationStage"; | ||||||
| import { PasswordChallenge } from "./stages/password/PasswordStage"; | import { PasswordChallenge } from "./stages/password/PasswordStage"; | ||||||
| @ -44,6 +45,7 @@ import { AccessDeniedChallenge } from "./access_denied/FlowAccessDenied"; | |||||||
| import { PFSize } from "../elements/Spinner"; | import { PFSize } from "../elements/Spinner"; | ||||||
| import { TITLE_DEFAULT } from "../constants"; | import { TITLE_DEFAULT } from "../constants"; | ||||||
| import { configureSentry } from "../api/Sentry"; | import { configureSentry } from "../api/Sentry"; | ||||||
|  | import { PlexAuthenticationChallenge } from "./sources/plex/PlexLoginInit"; | ||||||
|  |  | ||||||
| @customElement("ak-flow-executor") | @customElement("ak-flow-executor") | ||||||
| export class FlowExecutor extends LitElement implements StageHost { | export class FlowExecutor extends LitElement implements StageHost { | ||||||
| @ -223,6 +225,8 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|                         return html`<ak-stage-authenticator-webauthn .host=${this} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn>`; |                         return html`<ak-stage-authenticator-webauthn .host=${this} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn>`; | ||||||
|                     case "ak-stage-authenticator-validate": |                     case "ak-stage-authenticator-validate": | ||||||
|                         return html`<ak-stage-authenticator-validate .host=${this} .challenge=${this.challenge as AuthenticatorValidateStageChallenge}></ak-stage-authenticator-validate>`; |                         return html`<ak-stage-authenticator-validate .host=${this} .challenge=${this.challenge as AuthenticatorValidateStageChallenge}></ak-stage-authenticator-validate>`; | ||||||
|  |                     case "ak-flow-sources-plex": | ||||||
|  |                         return html`<ak-flow-sources-plex .host=${this} .challenge=${this.challenge as PlexAuthenticationChallenge}></ak-flow-sources-plex>`; | ||||||
|                     default: |                     default: | ||||||
|                         break; |                         break; | ||||||
|                 } |                 } | ||||||
|  | |||||||
							
								
								
									
										95
									
								
								web/src/flows/sources/plex/API.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								web/src/flows/sources/plex/API.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | |||||||
|  | import { VERSION } from "../../../constants"; | ||||||
|  |  | ||||||
|  | export interface PlexPinResponse { | ||||||
|  |     // Only has the fields we care about | ||||||
|  |     authToken?: string; | ||||||
|  |     code: string; | ||||||
|  |     id: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PlexResource { | ||||||
|  |     name: string; | ||||||
|  |     provides: string; | ||||||
|  |     clientIdentifier: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const DEFAULT_HEADERS = { | ||||||
|  |     "Accept": "application/json", | ||||||
|  |     "Content-Type": "application/json", | ||||||
|  |     "X-Plex-Product": "authentik", | ||||||
|  |     "X-Plex-Version": VERSION, | ||||||
|  |     "X-Plex-Device-Vendor": "BeryJu.org", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null { | ||||||
|  |     const top = (screen.height - h) / 4, left = (screen.width - w) / 2; | ||||||
|  |     const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`); | ||||||
|  |     return popup; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class PlexAPIClient { | ||||||
|  |  | ||||||
|  |     token: string; | ||||||
|  |  | ||||||
|  |     constructor(token: string) { | ||||||
|  |         this.token = token; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> { | ||||||
|  |         const headers = { ...DEFAULT_HEADERS, ...{ | ||||||
|  |             "X-Plex-Client-Identifier": clientIdentifier | ||||||
|  |         }}; | ||||||
|  |         const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", { | ||||||
|  |             method: "POST", | ||||||
|  |             headers: headers | ||||||
|  |         }); | ||||||
|  |         const pin: PlexPinResponse = await pinResponse.json(); | ||||||
|  |         return { | ||||||
|  |             authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`, | ||||||
|  |             pin: pin | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> { | ||||||
|  |         const headers = { ...DEFAULT_HEADERS, ...{ | ||||||
|  |             "X-Plex-Client-Identifier": clientIdentifier | ||||||
|  |         }}; | ||||||
|  |         const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, { | ||||||
|  |             headers: headers | ||||||
|  |         }); | ||||||
|  |         const pin: PlexPinResponse = await pinResponse.json(); | ||||||
|  |         return pin.authToken || ""; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     static async pinPoll(clientIdentifier: string, id: number): Promise<string> { | ||||||
|  |         const executePoll = async ( | ||||||
|  |             resolve: (authToken: string) => void, | ||||||
|  |             reject: (e: Error) => void | ||||||
|  |         ) => { | ||||||
|  |             try { | ||||||
|  |                 const response = await PlexAPIClient.pinStatus(clientIdentifier, id); | ||||||
|  |  | ||||||
|  |                 if (response) { | ||||||
|  |                     resolve(response); | ||||||
|  |                 } else { | ||||||
|  |                     setTimeout(executePoll, 500, resolve, reject); | ||||||
|  |                 } | ||||||
|  |             } catch (e) { | ||||||
|  |                 reject(e); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return new Promise(executePoll); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getServers(): Promise<PlexResource[]> { | ||||||
|  |         const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, { | ||||||
|  |             headers: DEFAULT_HEADERS | ||||||
|  |         }); | ||||||
|  |         const resources: PlexResource[] = await resourcesResponse.json(); | ||||||
|  |         return resources.filter(r => { | ||||||
|  |             return r.provides === "server"; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								web/src/flows/sources/plex/PlexLoginInit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								web/src/flows/sources/plex/PlexLoginInit.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | import { t } from "@lingui/macro"; | ||||||
|  | import { Challenge } from "authentik-api"; | ||||||
|  | import PFLogin from "@patternfly/patternfly/components/Login/login.css"; | ||||||
|  | import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||||
|  | import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||||
|  | import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | ||||||
|  | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  | import AKGlobal from "../../../authentik.css"; | ||||||
|  | import { CSSResult, customElement, property } from "lit-element"; | ||||||
|  | import { html, TemplateResult } from "lit-html"; | ||||||
|  | import { BaseStage } from "../../stages/base"; | ||||||
|  | import { PlexAPIClient, popupCenterScreen } from "./API"; | ||||||
|  | import { DEFAULT_CONFIG } from "../../../api/Config"; | ||||||
|  | import { SourcesApi } from "authentik-api"; | ||||||
|  |  | ||||||
|  | export interface PlexAuthenticationChallenge extends Challenge { | ||||||
|  |  | ||||||
|  |     client_id: string; | ||||||
|  |     slug: string; | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @customElement("ak-flow-sources-plex") | ||||||
|  | export class PlexLoginInit extends BaseStage { | ||||||
|  |  | ||||||
|  |     @property({ attribute: false }) | ||||||
|  |     challenge?: PlexAuthenticationChallenge; | ||||||
|  |  | ||||||
|  |     static get styles(): CSSResult[] { | ||||||
|  |         return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async firstUpdated(): Promise<void> { | ||||||
|  |         const authInfo = await PlexAPIClient.getPin(this.challenge?.client_id || ""); | ||||||
|  |         const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700); | ||||||
|  |         PlexAPIClient.pinPoll(this.challenge?.client_id || "", authInfo.pin.id).then(token => { | ||||||
|  |             authWindow?.close(); | ||||||
|  |             new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemToken({ | ||||||
|  |                 data: { | ||||||
|  |                     plexToken: token, | ||||||
|  |                 }, | ||||||
|  |                 slug: this.challenge?.slug || "", | ||||||
|  |             }).then(r => { | ||||||
|  |                 window.location.assign(r.to); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render(): TemplateResult { | ||||||
|  |         return html`<header class="pf-c-login__main-header"> | ||||||
|  |                 <h1 class="pf-c-title pf-m-3xl"> | ||||||
|  |                     ${t`Authenticating with Plex...`} | ||||||
|  |                 </h1> | ||||||
|  |             </header> | ||||||
|  |             <div class="pf-c-login__main-body"> | ||||||
|  |                 <form class="pf-c-form"> | ||||||
|  |                     <ak-empty-state | ||||||
|  |                         ?loading="${true}"> | ||||||
|  |                     </ak-empty-state> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |             <footer class="pf-c-login__main-footer"> | ||||||
|  |                 <ul class="pf-c-login__main-footer-links"> | ||||||
|  |                 </ul> | ||||||
|  |             </footer>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @ -1,6 +1,8 @@ | |||||||
|  | import { Challenge } from "authentik-api"; | ||||||
| import { LitElement } from "lit-element"; | import { LitElement } from "lit-element"; | ||||||
|  |  | ||||||
| export interface StageHost { | export interface StageHost { | ||||||
|  |     challenge?: Challenge; | ||||||
|     submit<T>(formData?: T): Promise<void>; |     submit<T>(formData?: T): Promise<void>; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -35,7 +35,7 @@ export interface IdentificationChallenge extends Challenge { | |||||||
|  |  | ||||||
| export interface UILoginButton { | export interface UILoginButton { | ||||||
|     name: string; |     name: string; | ||||||
|     url: string; |     challenge: Challenge; | ||||||
|     icon_url?: string; |     icon_url?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -49,7 +49,11 @@ export class IdentificationStage extends BaseStage { | |||||||
|         return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat( |         return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat( | ||||||
|             css` |             css` | ||||||
|                 /* login page's icons */ |                 /* login page's icons */ | ||||||
|                 .pf-c-login__main-footer-links-item-link img { |                 .pf-c-login__main-footer-links-item button { | ||||||
|  |                     background-color: transparent; | ||||||
|  |                     border: 0; | ||||||
|  |                 } | ||||||
|  |                 .pf-c-login__main-footer-links-item img { | ||||||
|                     fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill); |                     fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill); | ||||||
|                     width: 100px; |                     width: 100px; | ||||||
|                     max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width); |                     max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width); | ||||||
| @ -131,9 +135,12 @@ export class IdentificationStage extends BaseStage { | |||||||
|             icon = html`<img src="${source.icon_url}" alt="${source.name}">`; |             icon = html`<img src="${source.icon_url}" alt="${source.name}">`; | ||||||
|         } |         } | ||||||
|         return html`<li class="pf-c-login__main-footer-links-item"> |         return html`<li class="pf-c-login__main-footer-links-item"> | ||||||
|                 <a href="${source.url}" class="pf-c-login__main-footer-links-item-link"> |                 <button type="button" @click=${() => { | ||||||
|  |                     if (!this.host) return; | ||||||
|  |                     this.host.challenge = source.challenge; | ||||||
|  |                 }}> | ||||||
|                     ${icon} |                     ${icon} | ||||||
|                 </a> |                 </button> | ||||||
|             </li>`; |             </li>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -156,6 +156,10 @@ msgstr "Allow users to use Applications based on properties, enforce Password Cr | |||||||
| msgid "Allowed count" | msgid "Allowed count" | ||||||
| msgstr "Allowed count" | msgstr "Allowed count" | ||||||
|  |  | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:119 | ||||||
|  | msgid "Allowed servers" | ||||||
|  | msgstr "Allowed servers" | ||||||
|  |  | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:144 | #: src/pages/sources/saml/SAMLSourceForm.ts:144 | ||||||
| msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." | msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." | ||||||
| msgstr "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." | msgstr "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." | ||||||
| @ -277,11 +281,16 @@ msgstr "Attributes" | |||||||
| msgid "Audience" | msgid "Audience" | ||||||
| msgstr "Audience" | msgstr "Audience" | ||||||
|  |  | ||||||
|  | #: src/flows/sources/plex/PlexLoginInit.ts:56 | ||||||
|  | msgid "Authenticating with Plex..." | ||||||
|  | msgstr "Authenticating with Plex..." | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts:55 | #: src/pages/flows/FlowForm.ts:55 | ||||||
| msgid "Authentication" | msgid "Authentication" | ||||||
| msgstr "Authentication" | msgstr "Authentication" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:189 | #: src/pages/sources/oauth/OAuthSourceForm.ts:189 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:149 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:245 | #: src/pages/sources/saml/SAMLSourceForm.ts:245 | ||||||
| msgid "Authentication flow" | msgid "Authentication flow" | ||||||
| msgstr "Authentication flow" | msgstr "Authentication flow" | ||||||
| @ -395,8 +404,8 @@ msgstr "Binding Type" | |||||||
| msgid "Build hash: {0}" | msgid "Build hash: {0}" | ||||||
| msgstr "Build hash: {0}" | msgstr "Build hash: {0}" | ||||||
|  |  | ||||||
| #: src/pages/sources/SourcesListPage.ts:103 | #: src/pages/sources/SourcesListPage.ts:104 | ||||||
| #: src/pages/sources/SourcesListPage.ts:105 | #: src/pages/sources/SourcesListPage.ts:106 | ||||||
| msgid "Built-in" | msgid "Built-in" | ||||||
| msgstr "Built-in" | msgstr "Built-in" | ||||||
|  |  | ||||||
| @ -544,6 +553,7 @@ msgstr "Click to copy token" | |||||||
|  |  | ||||||
| #: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107 | #: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107 | ||||||
| #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99 | #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:113 | ||||||
| msgid "Client ID" | msgid "Client ID" | ||||||
| msgstr "Client ID" | msgstr "Client ID" | ||||||
|  |  | ||||||
| @ -744,8 +754,8 @@ msgstr "Copy Key" | |||||||
| #: src/pages/providers/ProviderListPage.ts:116 | #: src/pages/providers/ProviderListPage.ts:116 | ||||||
| #: src/pages/providers/RelatedApplicationButton.ts:27 | #: src/pages/providers/RelatedApplicationButton.ts:27 | ||||||
| #: src/pages/providers/RelatedApplicationButton.ts:35 | #: src/pages/providers/RelatedApplicationButton.ts:35 | ||||||
| #: src/pages/sources/SourcesListPage.ts:113 | #: src/pages/sources/SourcesListPage.ts:114 | ||||||
| #: src/pages/sources/SourcesListPage.ts:122 | #: src/pages/sources/SourcesListPage.ts:123 | ||||||
| #: src/pages/stages/StageListPage.ts:119 | #: src/pages/stages/StageListPage.ts:119 | ||||||
| #: src/pages/stages/StageListPage.ts:128 | #: src/pages/stages/StageListPage.ts:128 | ||||||
| #: src/pages/stages/invitation/InvitationListPage.ts:77 | #: src/pages/stages/invitation/InvitationListPage.ts:77 | ||||||
| @ -842,7 +852,7 @@ msgstr "Create provider" | |||||||
| #: src/pages/policies/PolicyListPage.ts:136 | #: src/pages/policies/PolicyListPage.ts:136 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:125 | #: src/pages/property-mappings/PropertyMappingListPage.ts:125 | ||||||
| #: src/pages/providers/ProviderListPage.ts:119 | #: src/pages/providers/ProviderListPage.ts:119 | ||||||
| #: src/pages/sources/SourcesListPage.ts:125 | #: src/pages/sources/SourcesListPage.ts:126 | ||||||
| #: src/pages/stages/StageListPage.ts:131 | #: src/pages/stages/StageListPage.ts:131 | ||||||
| msgid "Create {0}" | msgid "Create {0}" | ||||||
| msgstr "Create {0}" | msgstr "Create {0}" | ||||||
| @ -898,7 +908,7 @@ msgstr "Define how notifications are sent to users, like Email or Webhook." | |||||||
| #: src/pages/policies/PolicyListPage.ts:115 | #: src/pages/policies/PolicyListPage.ts:115 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:104 | #: src/pages/property-mappings/PropertyMappingListPage.ts:104 | ||||||
| #: src/pages/providers/ProviderListPage.ts:98 | #: src/pages/providers/ProviderListPage.ts:98 | ||||||
| #: src/pages/sources/SourcesListPage.ts:94 | #: src/pages/sources/SourcesListPage.ts:95 | ||||||
| #: src/pages/stages/StageListPage.ts:110 | #: src/pages/stages/StageListPage.ts:110 | ||||||
| #: src/pages/stages/invitation/InvitationListPage.ts:68 | #: src/pages/stages/invitation/InvitationListPage.ts:68 | ||||||
| #: src/pages/stages/prompt/PromptListPage.ts:87 | #: src/pages/stages/prompt/PromptListPage.ts:87 | ||||||
| @ -1008,7 +1018,7 @@ msgstr "Disable Static Tokens" | |||||||
| msgid "Disable Time-based OTP" | msgid "Disable Time-based OTP" | ||||||
| msgstr "Disable Time-based OTP" | msgstr "Disable Time-based OTP" | ||||||
|  |  | ||||||
| #: src/pages/sources/SourcesListPage.ts:63 | #: src/pages/sources/SourcesListPage.ts:64 | ||||||
| msgid "Disabled" | msgid "Disabled" | ||||||
| msgstr "Disabled" | msgstr "Disabled" | ||||||
|  |  | ||||||
| @ -1049,7 +1059,7 @@ msgstr "Each provider has a different issuer, based on the application slug." | |||||||
| #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128 | #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128 | ||||||
| #: src/pages/providers/proxy/ProxyProviderViewPage.ts:127 | #: src/pages/providers/proxy/ProxyProviderViewPage.ts:127 | ||||||
| #: src/pages/providers/saml/SAMLProviderViewPage.ts:121 | #: src/pages/providers/saml/SAMLProviderViewPage.ts:121 | ||||||
| #: src/pages/sources/SourcesListPage.ts:82 | #: src/pages/sources/SourcesListPage.ts:83 | ||||||
| #: src/pages/sources/ldap/LDAPSourceViewPage.ts:105 | #: src/pages/sources/ldap/LDAPSourceViewPage.ts:105 | ||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts:125 | #: src/pages/sources/oauth/OAuthSourceViewPage.ts:125 | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts:111 | #: src/pages/sources/saml/SAMLSourceViewPage.ts:111 | ||||||
| @ -1086,7 +1096,7 @@ msgstr "Edit User" | |||||||
| msgid "Either no applications are defined, or you don't have access to any." | msgid "Either no applications are defined, or you don't have access to any." | ||||||
| msgstr "Either no applications are defined, or you don't have access to any." | msgstr "Either no applications are defined, or you don't have access to any." | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:138 | #: src/flows/stages/identification/IdentificationStage.ts:146 | ||||||
| #: src/pages/events/TransportForm.ts:46 | #: src/pages/events/TransportForm.ts:46 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:81 | #: src/pages/stages/identification/IdentificationStageForm.ts:81 | ||||||
| #: src/pages/user-settings/UserDetailsPage.ts:71 | #: src/pages/user-settings/UserDetailsPage.ts:71 | ||||||
| @ -1099,7 +1109,7 @@ msgstr "Email" | |||||||
| msgid "Email address" | msgid "Email address" | ||||||
| msgstr "Email address" | msgstr "Email address" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:145 | #: src/flows/stages/identification/IdentificationStage.ts:153 | ||||||
| msgid "Email or username" | msgid "Email or username" | ||||||
| msgstr "Email or username" | msgstr "Email or username" | ||||||
|  |  | ||||||
| @ -1136,6 +1146,7 @@ msgstr "Enable this if you don't want to use this provider as a proxy, and want | |||||||
| #: src/pages/policies/PolicyBindingForm.ts:199 | #: src/pages/policies/PolicyBindingForm.ts:199 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:69 | #: src/pages/sources/ldap/LDAPSourceForm.ts:69 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:115 | #: src/pages/sources/oauth/OAuthSourceForm.ts:115 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:102 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:69 | #: src/pages/sources/saml/SAMLSourceForm.ts:69 | ||||||
| msgid "Enabled" | msgid "Enabled" | ||||||
| msgstr "Enabled" | msgstr "Enabled" | ||||||
| @ -1145,6 +1156,7 @@ msgid "Enrollment" | |||||||
| msgstr "Enrollment" | msgstr "Enrollment" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:210 | #: src/pages/sources/oauth/OAuthSourceForm.ts:210 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:170 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:266 | #: src/pages/sources/saml/SAMLSourceForm.ts:266 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:106 | #: src/pages/stages/identification/IdentificationStageForm.ts:106 | ||||||
| msgid "Enrollment flow" | msgid "Enrollment flow" | ||||||
| @ -1357,16 +1369,19 @@ msgid "Flow Overview" | |||||||
| msgstr "Flow Overview" | msgstr "Flow Overview" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:185 | #: src/pages/sources/oauth/OAuthSourceForm.ts:185 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:145 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:220 | #: src/pages/sources/saml/SAMLSourceForm.ts:220 | ||||||
| msgid "Flow settings" | msgid "Flow settings" | ||||||
| msgstr "Flow settings" | msgstr "Flow settings" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:207 | #: src/pages/sources/oauth/OAuthSourceForm.ts:207 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:167 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:263 | #: src/pages/sources/saml/SAMLSourceForm.ts:263 | ||||||
| msgid "Flow to use when authenticating existing users." | msgid "Flow to use when authenticating existing users." | ||||||
| msgstr "Flow to use when authenticating existing users." | msgstr "Flow to use when authenticating existing users." | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:228 | #: src/pages/sources/oauth/OAuthSourceForm.ts:228 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:188 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:284 | #: src/pages/sources/saml/SAMLSourceForm.ts:284 | ||||||
| msgid "Flow to use when enrolling new users." | msgid "Flow to use when enrolling new users." | ||||||
| msgstr "Flow to use when enrolling new users." | msgstr "Flow to use when enrolling new users." | ||||||
| @ -1410,7 +1425,7 @@ msgstr "Force the user to configure an authenticator" | |||||||
| msgid "Forgot password?" | msgid "Forgot password?" | ||||||
| msgstr "Forgot password?" | msgstr "Forgot password?" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:124 | #: src/flows/stages/identification/IdentificationStage.ts:132 | ||||||
| msgid "Forgot username or password?" | msgid "Forgot username or password?" | ||||||
| msgstr "Forgot username or password?" | msgstr "Forgot username or password?" | ||||||
|  |  | ||||||
| @ -1510,6 +1525,7 @@ msgstr "Hide managed mappings" | |||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts:177 | #: src/pages/providers/saml/SAMLProviderForm.ts:177 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:167 | #: src/pages/sources/ldap/LDAPSourceForm.ts:167 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:193 | #: src/pages/sources/ldap/LDAPSourceForm.ts:193 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:132 | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114 | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:85 | #: src/pages/stages/identification/IdentificationStageForm.ts:85 | ||||||
| #: src/pages/stages/password/PasswordStageForm.ts:86 | #: src/pages/stages/password/PasswordStageForm.ts:86 | ||||||
| @ -1692,9 +1708,13 @@ msgstr "Let the user identify themselves with their username or Email address." | |||||||
| msgid "Library" | msgid "Library" | ||||||
| msgstr "Library" | msgstr "Library" | ||||||
|  |  | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:137 | ||||||
|  | msgid "Load servers" | ||||||
|  | msgstr "Load servers" | ||||||
|  |  | ||||||
| #: src/elements/table/Table.ts:120 | #: src/elements/table/Table.ts:120 | ||||||
| #: src/flows/FlowExecutor.ts:167 | #: src/flows/FlowExecutor.ts:168 | ||||||
| #: src/flows/FlowExecutor.ts:213 | #: src/flows/FlowExecutor.ts:216 | ||||||
| #: src/flows/access_denied/FlowAccessDenied.ts:27 | #: src/flows/access_denied/FlowAccessDenied.ts:27 | ||||||
| #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43 | #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43 | ||||||
| #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33 | #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33 | ||||||
| @ -1705,7 +1725,7 @@ msgstr "Library" | |||||||
| #: src/flows/stages/consent/ConsentStage.ts:28 | #: src/flows/stages/consent/ConsentStage.ts:28 | ||||||
| #: src/flows/stages/dummy/DummyStage.ts:27 | #: src/flows/stages/dummy/DummyStage.ts:27 | ||||||
| #: src/flows/stages/email/EmailStage.ts:26 | #: src/flows/stages/email/EmailStage.ts:26 | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:171 | #: src/flows/stages/identification/IdentificationStage.ts:179 | ||||||
| #: src/flows/stages/password/PasswordStage.ts:31 | #: src/flows/stages/password/PasswordStage.ts:31 | ||||||
| #: src/flows/stages/prompt/PromptStage.ts:126 | #: src/flows/stages/prompt/PromptStage.ts:126 | ||||||
| #: src/pages/applications/ApplicationViewPage.ts:43 | #: src/pages/applications/ApplicationViewPage.ts:43 | ||||||
| @ -1750,6 +1770,8 @@ msgstr "Loading" | |||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:177 | #: src/pages/sources/oauth/OAuthSourceForm.ts:177 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:205 | #: src/pages/sources/oauth/OAuthSourceForm.ts:205 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:226 | #: src/pages/sources/oauth/OAuthSourceForm.ts:226 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:165 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:186 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:126 | #: src/pages/sources/saml/SAMLSourceForm.ts:126 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:240 | #: src/pages/sources/saml/SAMLSourceForm.ts:240 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:261 | #: src/pages/sources/saml/SAMLSourceForm.ts:261 | ||||||
| @ -1780,7 +1802,7 @@ msgstr "Log the currently pending user in." | |||||||
| msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." | msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." | ||||||
| msgstr "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." | msgstr "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:183 | #: src/flows/stages/identification/IdentificationStage.ts:191 | ||||||
| msgid "Login to continue to {0}." | msgid "Login to continue to {0}." | ||||||
| msgstr "Login to continue to {0}." | msgstr "Login to continue to {0}." | ||||||
|  |  | ||||||
| @ -1913,11 +1935,12 @@ msgstr "Monitor" | |||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts:53 | #: src/pages/providers/saml/SAMLProviderForm.ts:53 | ||||||
| #: src/pages/providers/saml/SAMLProviderImportForm.ts:38 | #: src/pages/providers/saml/SAMLProviderImportForm.ts:38 | ||||||
| #: src/pages/providers/saml/SAMLProviderViewPage.ts:66 | #: src/pages/providers/saml/SAMLProviderViewPage.ts:66 | ||||||
| #: src/pages/sources/SourcesListPage.ts:51 | #: src/pages/sources/SourcesListPage.ts:52 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:54 | #: src/pages/sources/ldap/LDAPSourceForm.ts:54 | ||||||
| #: src/pages/sources/ldap/LDAPSourceViewPage.ts:64 | #: src/pages/sources/ldap/LDAPSourceViewPage.ts:64 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:100 | #: src/pages/sources/oauth/OAuthSourceForm.ts:100 | ||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts:64 | #: src/pages/sources/oauth/OAuthSourceViewPage.ts:64 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:87 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:54 | #: src/pages/sources/saml/SAMLSourceForm.ts:54 | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts:66 | #: src/pages/sources/saml/SAMLSourceViewPage.ts:66 | ||||||
| #: src/pages/stages/StageListPage.ts:65 | #: src/pages/stages/StageListPage.ts:65 | ||||||
| @ -1957,7 +1980,7 @@ msgstr "NameID Policy" | |||||||
| msgid "NameID Property Mapping" | msgid "NameID Property Mapping" | ||||||
| msgstr "NameID Property Mapping" | msgstr "NameID Property Mapping" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:119 | #: src/flows/stages/identification/IdentificationStage.ts:127 | ||||||
| msgid "Need an account?" | msgid "Need an account?" | ||||||
| msgstr "Need an account?" | msgstr "Need an account?" | ||||||
|  |  | ||||||
| @ -2348,7 +2371,7 @@ msgstr "Post binding" | |||||||
| msgid "Post binding (auto-submit)" | msgid "Post binding (auto-submit)" | ||||||
| msgstr "Post binding (auto-submit)" | msgstr "Post binding (auto-submit)" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:255 | #: src/flows/FlowExecutor.ts:258 | ||||||
| msgid "Powered by authentik" | msgid "Powered by authentik" | ||||||
| msgstr "Powered by authentik" | msgstr "Powered by authentik" | ||||||
|  |  | ||||||
| @ -2412,6 +2435,7 @@ msgstr "Property mappings used to user creation." | |||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts:123 | #: src/pages/providers/proxy/ProxyProviderForm.ts:123 | ||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts:78 | #: src/pages/providers/saml/SAMLProviderForm.ts:78 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:122 | #: src/pages/sources/oauth/OAuthSourceForm.ts:122 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:109 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:76 | #: src/pages/sources/saml/SAMLSourceForm.ts:76 | ||||||
| msgid "Protocol settings" | msgid "Protocol settings" | ||||||
| msgstr "Protocol settings" | msgstr "Protocol settings" | ||||||
| @ -2602,7 +2626,7 @@ msgstr "Retry Task" | |||||||
| msgid "Retry authentication" | msgid "Retry authentication" | ||||||
| msgstr "Retry authentication" | msgstr "Retry authentication" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:145 | #: src/flows/FlowExecutor.ts:146 | ||||||
| msgid "Return" | msgid "Return" | ||||||
| msgstr "Return" | msgstr "Return" | ||||||
|  |  | ||||||
| @ -2710,7 +2734,7 @@ msgstr "Select all rows" | |||||||
| msgid "Select an identification method." | msgid "Select an identification method." | ||||||
| msgstr "Select an identification method." | msgstr "Select an identification method." | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:134 | #: src/flows/stages/identification/IdentificationStage.ts:142 | ||||||
| msgid "Select one of the sources below to login." | msgid "Select one of the sources below to login." | ||||||
| msgstr "Select one of the sources below to login." | msgstr "Select one of the sources below to login." | ||||||
|  |  | ||||||
| @ -2722,6 +2746,10 @@ msgstr "Select users to add" | |||||||
| msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." | msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." | ||||||
| msgstr "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." | msgstr "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." | ||||||
|  |  | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:131 | ||||||
|  | msgid "Select which server a user has to be a member of to be allowed to authenticate." | ||||||
|  | msgstr "Select which server a user has to be a member of to be allowed to authenticate." | ||||||
|  |  | ||||||
| #: src/pages/events/RuleForm.ts:92 | #: src/pages/events/RuleForm.ts:92 | ||||||
| msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." | msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." | ||||||
| msgstr "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." | msgstr "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." | ||||||
| @ -2820,7 +2848,7 @@ msgstr "Show matched user" | |||||||
| msgid "Shown as the Title in Flow pages." | msgid "Shown as the Title in Flow pages." | ||||||
| msgstr "Shown as the Title in Flow pages." | msgstr "Shown as the Title in Flow pages." | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:120 | #: src/flows/stages/identification/IdentificationStage.ts:128 | ||||||
| msgid "Sign up." | msgid "Sign up." | ||||||
| msgstr "Sign up." | msgstr "Sign up." | ||||||
|  |  | ||||||
| @ -2841,6 +2869,10 @@ msgstr "Signing keypair" | |||||||
| msgid "Single Prompts that can be used for Prompt Stages." | msgid "Single Prompts that can be used for Prompt Stages." | ||||||
| msgstr "Single Prompts that can be used for Prompt Stages." | msgstr "Single Prompts that can be used for Prompt Stages." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/invitation/InvitationForm.ts:62 | ||||||
|  | msgid "Single use" | ||||||
|  | msgstr "Single use" | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts:173 | #: src/pages/providers/proxy/ProxyProviderForm.ts:173 | ||||||
| msgid "Skip path regex" | msgid "Skip path regex" | ||||||
| msgstr "Skip path regex" | msgstr "Skip path regex" | ||||||
| @ -2850,16 +2882,17 @@ msgstr "Skip path regex" | |||||||
| #: src/pages/flows/FlowForm.ts:94 | #: src/pages/flows/FlowForm.ts:94 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:60 | #: src/pages/sources/ldap/LDAPSourceForm.ts:60 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:106 | #: src/pages/sources/oauth/OAuthSourceForm.ts:106 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:93 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:60 | #: src/pages/sources/saml/SAMLSourceForm.ts:60 | ||||||
| msgid "Slug" | msgid "Slug" | ||||||
| msgstr "Slug" | msgstr "Slug" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:138 | #: src/flows/FlowExecutor.ts:139 | ||||||
| msgid "Something went wrong! Please try again later." | msgid "Something went wrong! Please try again later." | ||||||
| msgstr "Something went wrong! Please try again later." | msgstr "Something went wrong! Please try again later." | ||||||
|  |  | ||||||
| #: src/pages/providers/ProviderListPage.ts:91 | #: src/pages/providers/ProviderListPage.ts:91 | ||||||
| #: src/pages/sources/SourcesListPage.ts:87 | #: src/pages/sources/SourcesListPage.ts:88 | ||||||
| msgid "Source" | msgid "Source" | ||||||
| msgstr "Source" | msgstr "Source" | ||||||
|  |  | ||||||
| @ -2868,11 +2901,11 @@ msgid "Source {0}" | |||||||
| msgstr "Source {0}" | msgstr "Source {0}" | ||||||
|  |  | ||||||
| #: src/interfaces/AdminInterface.ts:20 | #: src/interfaces/AdminInterface.ts:20 | ||||||
| #: src/pages/sources/SourcesListPage.ts:30 | #: src/pages/sources/SourcesListPage.ts:31 | ||||||
| msgid "Sources" | msgid "Sources" | ||||||
| msgstr "Sources" | msgstr "Sources" | ||||||
|  |  | ||||||
| #: src/pages/sources/SourcesListPage.ts:33 | #: src/pages/sources/SourcesListPage.ts:34 | ||||||
| msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" | msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" | ||||||
| msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" | msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" | ||||||
|  |  | ||||||
| @ -3069,6 +3102,7 @@ msgstr "Successfully created service-connection." | |||||||
|  |  | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:47 | #: src/pages/sources/ldap/LDAPSourceForm.ts:47 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:51 | #: src/pages/sources/oauth/OAuthSourceForm.ts:51 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:60 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:47 | #: src/pages/sources/saml/SAMLSourceForm.ts:47 | ||||||
| msgid "Successfully created source." | msgid "Successfully created source." | ||||||
| msgstr "Successfully created source." | msgstr "Successfully created source." | ||||||
| @ -3205,6 +3239,7 @@ msgstr "Successfully updated service-connection." | |||||||
|  |  | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:44 | #: src/pages/sources/ldap/LDAPSourceForm.ts:44 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:48 | #: src/pages/sources/oauth/OAuthSourceForm.ts:48 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:57 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:44 | #: src/pages/sources/saml/SAMLSourceForm.ts:44 | ||||||
| msgid "Successfully updated source." | msgid "Successfully updated source." | ||||||
| msgstr "Successfully updated source." | msgstr "Successfully updated source." | ||||||
| @ -3460,7 +3495,7 @@ msgstr "Transports" | |||||||
| #: src/pages/policies/PolicyListPage.ts:57 | #: src/pages/policies/PolicyListPage.ts:57 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:55 | #: src/pages/property-mappings/PropertyMappingListPage.ts:55 | ||||||
| #: src/pages/providers/ProviderListPage.ts:54 | #: src/pages/providers/ProviderListPage.ts:54 | ||||||
| #: src/pages/sources/SourcesListPage.ts:52 | #: src/pages/sources/SourcesListPage.ts:53 | ||||||
| #: src/pages/stages/prompt/PromptForm.ts:97 | #: src/pages/stages/prompt/PromptForm.ts:97 | ||||||
| #: src/pages/stages/prompt/PromptListPage.ts:48 | #: src/pages/stages/prompt/PromptListPage.ts:48 | ||||||
| msgid "Type" | msgid "Type" | ||||||
| @ -3539,7 +3574,7 @@ msgstr "Up-to-date!" | |||||||
| #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118 | #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118 | ||||||
| #: src/pages/providers/proxy/ProxyProviderViewPage.ts:117 | #: src/pages/providers/proxy/ProxyProviderViewPage.ts:117 | ||||||
| #: src/pages/providers/saml/SAMLProviderViewPage.ts:111 | #: src/pages/providers/saml/SAMLProviderViewPage.ts:111 | ||||||
| #: src/pages/sources/SourcesListPage.ts:69 | #: src/pages/sources/SourcesListPage.ts:70 | ||||||
| #: src/pages/sources/ldap/LDAPSourceViewPage.ts:95 | #: src/pages/sources/ldap/LDAPSourceViewPage.ts:95 | ||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts:115 | #: src/pages/sources/oauth/OAuthSourceViewPage.ts:115 | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts:101 | #: src/pages/sources/saml/SAMLSourceViewPage.ts:101 | ||||||
| @ -3642,7 +3677,7 @@ msgstr "Update details" | |||||||
| #: src/pages/policies/PolicyListPage.ts:80 | #: src/pages/policies/PolicyListPage.ts:80 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:69 | #: src/pages/property-mappings/PropertyMappingListPage.ts:69 | ||||||
| #: src/pages/providers/ProviderListPage.ts:76 | #: src/pages/providers/ProviderListPage.ts:76 | ||||||
| #: src/pages/sources/SourcesListPage.ts:72 | #: src/pages/sources/SourcesListPage.ts:73 | ||||||
| #: src/pages/stages/StageListPage.ts:88 | #: src/pages/stages/StageListPage.ts:88 | ||||||
| #: src/pages/users/UserActiveForm.ts:41 | #: src/pages/users/UserActiveForm.ts:41 | ||||||
| msgid "Update {0}" | msgid "Update {0}" | ||||||
| @ -3746,7 +3781,7 @@ msgstr "User/Group Attribute used for the user part of the HTTP-Basic Header. If | |||||||
| msgid "Userinfo URL" | msgid "Userinfo URL" | ||||||
| msgstr "Userinfo URL" | msgstr "Userinfo URL" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:142 | #: src/flows/stages/identification/IdentificationStage.ts:150 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:78 | #: src/pages/stages/identification/IdentificationStageForm.ts:78 | ||||||
| #: src/pages/user-settings/UserDetailsPage.ts:57 | #: src/pages/user-settings/UserDetailsPage.ts:57 | ||||||
| #: src/pages/users/UserForm.ts:47 | #: src/pages/users/UserForm.ts:47 | ||||||
| @ -3877,6 +3912,10 @@ msgstr "When a valid username/email has been entered, and this option is enabled | |||||||
| msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." | msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." | ||||||
| msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored." | msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/invitation/InvitationForm.ts:66 | ||||||
|  | msgid "When enabled, the invitation will be deleted after usage." | ||||||
|  | msgstr "When enabled, the invitation will be deleted after usage." | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:94 | #: src/pages/stages/identification/IdentificationStageForm.ts:94 | ||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "When enabled, user fields are matched regardless of their casing." | msgstr "When enabled, user fields are matched regardless of their casing." | ||||||
| @ -3895,7 +3934,7 @@ msgstr "When selected, incoming assertion's Signatures will be validated against | |||||||
| msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." | msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." | ||||||
| msgstr "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." | msgstr "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:134 | #: src/flows/FlowExecutor.ts:135 | ||||||
| msgid "Whoops!" | msgid "Whoops!" | ||||||
| msgstr "Whoops!" | msgstr "Whoops!" | ||||||
|  |  | ||||||
|  | |||||||
| @ -156,6 +156,10 @@ msgstr "" | |||||||
| msgid "Allowed count" | msgid "Allowed count" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:119 | ||||||
|  | msgid "Allowed servers" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:144 | #: src/pages/sources/saml/SAMLSourceForm.ts:144 | ||||||
| msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." | msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -273,11 +277,16 @@ msgstr "" | |||||||
| msgid "Audience" | msgid "Audience" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/flows/sources/plex/PlexLoginInit.ts:56 | ||||||
|  | msgid "Authenticating with Plex..." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts:55 | #: src/pages/flows/FlowForm.ts:55 | ||||||
| msgid "Authentication" | msgid "Authentication" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:189 | #: src/pages/sources/oauth/OAuthSourceForm.ts:189 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:149 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:245 | #: src/pages/sources/saml/SAMLSourceForm.ts:245 | ||||||
| msgid "Authentication flow" | msgid "Authentication flow" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -391,8 +400,8 @@ msgstr "" | |||||||
| msgid "Build hash: {0}" | msgid "Build hash: {0}" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/SourcesListPage.ts:103 | #: src/pages/sources/SourcesListPage.ts:104 | ||||||
| #: src/pages/sources/SourcesListPage.ts:105 | #: src/pages/sources/SourcesListPage.ts:106 | ||||||
| msgid "Built-in" | msgid "Built-in" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -538,6 +547,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107 | #: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107 | ||||||
| #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99 | #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:113 | ||||||
| msgid "Client ID" | msgid "Client ID" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -738,8 +748,8 @@ msgstr "" | |||||||
| #: src/pages/providers/ProviderListPage.ts:116 | #: src/pages/providers/ProviderListPage.ts:116 | ||||||
| #: src/pages/providers/RelatedApplicationButton.ts:27 | #: src/pages/providers/RelatedApplicationButton.ts:27 | ||||||
| #: src/pages/providers/RelatedApplicationButton.ts:35 | #: src/pages/providers/RelatedApplicationButton.ts:35 | ||||||
| #: src/pages/sources/SourcesListPage.ts:113 | #: src/pages/sources/SourcesListPage.ts:114 | ||||||
| #: src/pages/sources/SourcesListPage.ts:122 | #: src/pages/sources/SourcesListPage.ts:123 | ||||||
| #: src/pages/stages/StageListPage.ts:119 | #: src/pages/stages/StageListPage.ts:119 | ||||||
| #: src/pages/stages/StageListPage.ts:128 | #: src/pages/stages/StageListPage.ts:128 | ||||||
| #: src/pages/stages/invitation/InvitationListPage.ts:77 | #: src/pages/stages/invitation/InvitationListPage.ts:77 | ||||||
| @ -836,7 +846,7 @@ msgstr "" | |||||||
| #: src/pages/policies/PolicyListPage.ts:136 | #: src/pages/policies/PolicyListPage.ts:136 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:125 | #: src/pages/property-mappings/PropertyMappingListPage.ts:125 | ||||||
| #: src/pages/providers/ProviderListPage.ts:119 | #: src/pages/providers/ProviderListPage.ts:119 | ||||||
| #: src/pages/sources/SourcesListPage.ts:125 | #: src/pages/sources/SourcesListPage.ts:126 | ||||||
| #: src/pages/stages/StageListPage.ts:131 | #: src/pages/stages/StageListPage.ts:131 | ||||||
| msgid "Create {0}" | msgid "Create {0}" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -892,7 +902,7 @@ msgstr "" | |||||||
| #: src/pages/policies/PolicyListPage.ts:115 | #: src/pages/policies/PolicyListPage.ts:115 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:104 | #: src/pages/property-mappings/PropertyMappingListPage.ts:104 | ||||||
| #: src/pages/providers/ProviderListPage.ts:98 | #: src/pages/providers/ProviderListPage.ts:98 | ||||||
| #: src/pages/sources/SourcesListPage.ts:94 | #: src/pages/sources/SourcesListPage.ts:95 | ||||||
| #: src/pages/stages/StageListPage.ts:110 | #: src/pages/stages/StageListPage.ts:110 | ||||||
| #: src/pages/stages/invitation/InvitationListPage.ts:68 | #: src/pages/stages/invitation/InvitationListPage.ts:68 | ||||||
| #: src/pages/stages/prompt/PromptListPage.ts:87 | #: src/pages/stages/prompt/PromptListPage.ts:87 | ||||||
| @ -1000,7 +1010,7 @@ msgstr "" | |||||||
| msgid "Disable Time-based OTP" | msgid "Disable Time-based OTP" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/SourcesListPage.ts:63 | #: src/pages/sources/SourcesListPage.ts:64 | ||||||
| msgid "Disabled" | msgid "Disabled" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -1041,7 +1051,7 @@ msgstr "" | |||||||
| #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128 | #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128 | ||||||
| #: src/pages/providers/proxy/ProxyProviderViewPage.ts:127 | #: src/pages/providers/proxy/ProxyProviderViewPage.ts:127 | ||||||
| #: src/pages/providers/saml/SAMLProviderViewPage.ts:121 | #: src/pages/providers/saml/SAMLProviderViewPage.ts:121 | ||||||
| #: src/pages/sources/SourcesListPage.ts:82 | #: src/pages/sources/SourcesListPage.ts:83 | ||||||
| #: src/pages/sources/ldap/LDAPSourceViewPage.ts:105 | #: src/pages/sources/ldap/LDAPSourceViewPage.ts:105 | ||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts:125 | #: src/pages/sources/oauth/OAuthSourceViewPage.ts:125 | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts:111 | #: src/pages/sources/saml/SAMLSourceViewPage.ts:111 | ||||||
| @ -1078,7 +1088,7 @@ msgstr "" | |||||||
| msgid "Either no applications are defined, or you don't have access to any." | msgid "Either no applications are defined, or you don't have access to any." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:138 | #: src/flows/stages/identification/IdentificationStage.ts:146 | ||||||
| #: src/pages/events/TransportForm.ts:46 | #: src/pages/events/TransportForm.ts:46 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:81 | #: src/pages/stages/identification/IdentificationStageForm.ts:81 | ||||||
| #: src/pages/user-settings/UserDetailsPage.ts:71 | #: src/pages/user-settings/UserDetailsPage.ts:71 | ||||||
| @ -1091,7 +1101,7 @@ msgstr "" | |||||||
| msgid "Email address" | msgid "Email address" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:145 | #: src/flows/stages/identification/IdentificationStage.ts:153 | ||||||
| msgid "Email or username" | msgid "Email or username" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -1128,6 +1138,7 @@ msgstr "" | |||||||
| #: src/pages/policies/PolicyBindingForm.ts:199 | #: src/pages/policies/PolicyBindingForm.ts:199 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:69 | #: src/pages/sources/ldap/LDAPSourceForm.ts:69 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:115 | #: src/pages/sources/oauth/OAuthSourceForm.ts:115 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:102 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:69 | #: src/pages/sources/saml/SAMLSourceForm.ts:69 | ||||||
| msgid "Enabled" | msgid "Enabled" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1137,6 +1148,7 @@ msgid "Enrollment" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:210 | #: src/pages/sources/oauth/OAuthSourceForm.ts:210 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:170 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:266 | #: src/pages/sources/saml/SAMLSourceForm.ts:266 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:106 | #: src/pages/stages/identification/IdentificationStageForm.ts:106 | ||||||
| msgid "Enrollment flow" | msgid "Enrollment flow" | ||||||
| @ -1349,16 +1361,19 @@ msgid "Flow Overview" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:185 | #: src/pages/sources/oauth/OAuthSourceForm.ts:185 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:145 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:220 | #: src/pages/sources/saml/SAMLSourceForm.ts:220 | ||||||
| msgid "Flow settings" | msgid "Flow settings" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:207 | #: src/pages/sources/oauth/OAuthSourceForm.ts:207 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:167 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:263 | #: src/pages/sources/saml/SAMLSourceForm.ts:263 | ||||||
| msgid "Flow to use when authenticating existing users." | msgid "Flow to use when authenticating existing users." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:228 | #: src/pages/sources/oauth/OAuthSourceForm.ts:228 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:188 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:284 | #: src/pages/sources/saml/SAMLSourceForm.ts:284 | ||||||
| msgid "Flow to use when enrolling new users." | msgid "Flow to use when enrolling new users." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1402,7 +1417,7 @@ msgstr "" | |||||||
| msgid "Forgot password?" | msgid "Forgot password?" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:124 | #: src/flows/stages/identification/IdentificationStage.ts:132 | ||||||
| msgid "Forgot username or password?" | msgid "Forgot username or password?" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -1502,6 +1517,7 @@ msgstr "" | |||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts:177 | #: src/pages/providers/saml/SAMLProviderForm.ts:177 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:167 | #: src/pages/sources/ldap/LDAPSourceForm.ts:167 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:193 | #: src/pages/sources/ldap/LDAPSourceForm.ts:193 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:132 | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114 | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:85 | #: src/pages/stages/identification/IdentificationStageForm.ts:85 | ||||||
| #: src/pages/stages/password/PasswordStageForm.ts:86 | #: src/pages/stages/password/PasswordStageForm.ts:86 | ||||||
| @ -1684,9 +1700,13 @@ msgstr "" | |||||||
| msgid "Library" | msgid "Library" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:137 | ||||||
|  | msgid "Load servers" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/elements/table/Table.ts:120 | #: src/elements/table/Table.ts:120 | ||||||
| #: src/flows/FlowExecutor.ts:167 | #: src/flows/FlowExecutor.ts:168 | ||||||
| #: src/flows/FlowExecutor.ts:213 | #: src/flows/FlowExecutor.ts:216 | ||||||
| #: src/flows/access_denied/FlowAccessDenied.ts:27 | #: src/flows/access_denied/FlowAccessDenied.ts:27 | ||||||
| #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43 | #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43 | ||||||
| #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33 | #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33 | ||||||
| @ -1697,7 +1717,7 @@ msgstr "" | |||||||
| #: src/flows/stages/consent/ConsentStage.ts:28 | #: src/flows/stages/consent/ConsentStage.ts:28 | ||||||
| #: src/flows/stages/dummy/DummyStage.ts:27 | #: src/flows/stages/dummy/DummyStage.ts:27 | ||||||
| #: src/flows/stages/email/EmailStage.ts:26 | #: src/flows/stages/email/EmailStage.ts:26 | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:171 | #: src/flows/stages/identification/IdentificationStage.ts:179 | ||||||
| #: src/flows/stages/password/PasswordStage.ts:31 | #: src/flows/stages/password/PasswordStage.ts:31 | ||||||
| #: src/flows/stages/prompt/PromptStage.ts:126 | #: src/flows/stages/prompt/PromptStage.ts:126 | ||||||
| #: src/pages/applications/ApplicationViewPage.ts:43 | #: src/pages/applications/ApplicationViewPage.ts:43 | ||||||
| @ -1742,6 +1762,8 @@ msgstr "" | |||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:177 | #: src/pages/sources/oauth/OAuthSourceForm.ts:177 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:205 | #: src/pages/sources/oauth/OAuthSourceForm.ts:205 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:226 | #: src/pages/sources/oauth/OAuthSourceForm.ts:226 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:165 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:186 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:126 | #: src/pages/sources/saml/SAMLSourceForm.ts:126 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:240 | #: src/pages/sources/saml/SAMLSourceForm.ts:240 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:261 | #: src/pages/sources/saml/SAMLSourceForm.ts:261 | ||||||
| @ -1772,7 +1794,7 @@ msgstr "" | |||||||
| msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." | msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:183 | #: src/flows/stages/identification/IdentificationStage.ts:191 | ||||||
| msgid "Login to continue to {0}." | msgid "Login to continue to {0}." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -1905,11 +1927,12 @@ msgstr "" | |||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts:53 | #: src/pages/providers/saml/SAMLProviderForm.ts:53 | ||||||
| #: src/pages/providers/saml/SAMLProviderImportForm.ts:38 | #: src/pages/providers/saml/SAMLProviderImportForm.ts:38 | ||||||
| #: src/pages/providers/saml/SAMLProviderViewPage.ts:66 | #: src/pages/providers/saml/SAMLProviderViewPage.ts:66 | ||||||
| #: src/pages/sources/SourcesListPage.ts:51 | #: src/pages/sources/SourcesListPage.ts:52 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:54 | #: src/pages/sources/ldap/LDAPSourceForm.ts:54 | ||||||
| #: src/pages/sources/ldap/LDAPSourceViewPage.ts:64 | #: src/pages/sources/ldap/LDAPSourceViewPage.ts:64 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:100 | #: src/pages/sources/oauth/OAuthSourceForm.ts:100 | ||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts:64 | #: src/pages/sources/oauth/OAuthSourceViewPage.ts:64 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:87 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:54 | #: src/pages/sources/saml/SAMLSourceForm.ts:54 | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts:66 | #: src/pages/sources/saml/SAMLSourceViewPage.ts:66 | ||||||
| #: src/pages/stages/StageListPage.ts:65 | #: src/pages/stages/StageListPage.ts:65 | ||||||
| @ -1949,7 +1972,7 @@ msgstr "" | |||||||
| msgid "NameID Property Mapping" | msgid "NameID Property Mapping" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:119 | #: src/flows/stages/identification/IdentificationStage.ts:127 | ||||||
| msgid "Need an account?" | msgid "Need an account?" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -2340,7 +2363,7 @@ msgstr "" | |||||||
| msgid "Post binding (auto-submit)" | msgid "Post binding (auto-submit)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:255 | #: src/flows/FlowExecutor.ts:258 | ||||||
| msgid "Powered by authentik" | msgid "Powered by authentik" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -2404,6 +2427,7 @@ msgstr "" | |||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts:123 | #: src/pages/providers/proxy/ProxyProviderForm.ts:123 | ||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts:78 | #: src/pages/providers/saml/SAMLProviderForm.ts:78 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:122 | #: src/pages/sources/oauth/OAuthSourceForm.ts:122 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:109 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:76 | #: src/pages/sources/saml/SAMLSourceForm.ts:76 | ||||||
| msgid "Protocol settings" | msgid "Protocol settings" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2594,7 +2618,7 @@ msgstr "" | |||||||
| msgid "Retry authentication" | msgid "Retry authentication" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:145 | #: src/flows/FlowExecutor.ts:146 | ||||||
| msgid "Return" | msgid "Return" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -2702,7 +2726,7 @@ msgstr "" | |||||||
| msgid "Select an identification method." | msgid "Select an identification method." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:134 | #: src/flows/stages/identification/IdentificationStage.ts:142 | ||||||
| msgid "Select one of the sources below to login." | msgid "Select one of the sources below to login." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -2714,6 +2738,10 @@ msgstr "" | |||||||
| msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." | msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:131 | ||||||
|  | msgid "Select which server a user has to be a member of to be allowed to authenticate." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/events/RuleForm.ts:92 | #: src/pages/events/RuleForm.ts:92 | ||||||
| msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." | msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2812,7 +2840,7 @@ msgstr "" | |||||||
| msgid "Shown as the Title in Flow pages." | msgid "Shown as the Title in Flow pages." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:120 | #: src/flows/stages/identification/IdentificationStage.ts:128 | ||||||
| msgid "Sign up." | msgid "Sign up." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -2833,6 +2861,10 @@ msgstr "" | |||||||
| msgid "Single Prompts that can be used for Prompt Stages." | msgid "Single Prompts that can be used for Prompt Stages." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/invitation/InvitationForm.ts:62 | ||||||
|  | msgid "Single use" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts:173 | #: src/pages/providers/proxy/ProxyProviderForm.ts:173 | ||||||
| msgid "Skip path regex" | msgid "Skip path regex" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2842,16 +2874,17 @@ msgstr "" | |||||||
| #: src/pages/flows/FlowForm.ts:94 | #: src/pages/flows/FlowForm.ts:94 | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:60 | #: src/pages/sources/ldap/LDAPSourceForm.ts:60 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:106 | #: src/pages/sources/oauth/OAuthSourceForm.ts:106 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:93 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:60 | #: src/pages/sources/saml/SAMLSourceForm.ts:60 | ||||||
| msgid "Slug" | msgid "Slug" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:138 | #: src/flows/FlowExecutor.ts:139 | ||||||
| msgid "Something went wrong! Please try again later." | msgid "Something went wrong! Please try again later." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/ProviderListPage.ts:91 | #: src/pages/providers/ProviderListPage.ts:91 | ||||||
| #: src/pages/sources/SourcesListPage.ts:87 | #: src/pages/sources/SourcesListPage.ts:88 | ||||||
| msgid "Source" | msgid "Source" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -2860,11 +2893,11 @@ msgid "Source {0}" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/interfaces/AdminInterface.ts:20 | #: src/interfaces/AdminInterface.ts:20 | ||||||
| #: src/pages/sources/SourcesListPage.ts:30 | #: src/pages/sources/SourcesListPage.ts:31 | ||||||
| msgid "Sources" | msgid "Sources" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/sources/SourcesListPage.ts:33 | #: src/pages/sources/SourcesListPage.ts:34 | ||||||
| msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" | msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -3061,6 +3094,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:47 | #: src/pages/sources/ldap/LDAPSourceForm.ts:47 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:51 | #: src/pages/sources/oauth/OAuthSourceForm.ts:51 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:60 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:47 | #: src/pages/sources/saml/SAMLSourceForm.ts:47 | ||||||
| msgid "Successfully created source." | msgid "Successfully created source." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3197,6 +3231,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: src/pages/sources/ldap/LDAPSourceForm.ts:44 | #: src/pages/sources/ldap/LDAPSourceForm.ts:44 | ||||||
| #: src/pages/sources/oauth/OAuthSourceForm.ts:48 | #: src/pages/sources/oauth/OAuthSourceForm.ts:48 | ||||||
|  | #: src/pages/sources/plex/PlexSourceForm.ts:57 | ||||||
| #: src/pages/sources/saml/SAMLSourceForm.ts:44 | #: src/pages/sources/saml/SAMLSourceForm.ts:44 | ||||||
| msgid "Successfully updated source." | msgid "Successfully updated source." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3448,7 +3483,7 @@ msgstr "" | |||||||
| #: src/pages/policies/PolicyListPage.ts:57 | #: src/pages/policies/PolicyListPage.ts:57 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:55 | #: src/pages/property-mappings/PropertyMappingListPage.ts:55 | ||||||
| #: src/pages/providers/ProviderListPage.ts:54 | #: src/pages/providers/ProviderListPage.ts:54 | ||||||
| #: src/pages/sources/SourcesListPage.ts:52 | #: src/pages/sources/SourcesListPage.ts:53 | ||||||
| #: src/pages/stages/prompt/PromptForm.ts:97 | #: src/pages/stages/prompt/PromptForm.ts:97 | ||||||
| #: src/pages/stages/prompt/PromptListPage.ts:48 | #: src/pages/stages/prompt/PromptListPage.ts:48 | ||||||
| msgid "Type" | msgid "Type" | ||||||
| @ -3527,7 +3562,7 @@ msgstr "" | |||||||
| #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118 | #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118 | ||||||
| #: src/pages/providers/proxy/ProxyProviderViewPage.ts:117 | #: src/pages/providers/proxy/ProxyProviderViewPage.ts:117 | ||||||
| #: src/pages/providers/saml/SAMLProviderViewPage.ts:111 | #: src/pages/providers/saml/SAMLProviderViewPage.ts:111 | ||||||
| #: src/pages/sources/SourcesListPage.ts:69 | #: src/pages/sources/SourcesListPage.ts:70 | ||||||
| #: src/pages/sources/ldap/LDAPSourceViewPage.ts:95 | #: src/pages/sources/ldap/LDAPSourceViewPage.ts:95 | ||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts:115 | #: src/pages/sources/oauth/OAuthSourceViewPage.ts:115 | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts:101 | #: src/pages/sources/saml/SAMLSourceViewPage.ts:101 | ||||||
| @ -3630,7 +3665,7 @@ msgstr "" | |||||||
| #: src/pages/policies/PolicyListPage.ts:80 | #: src/pages/policies/PolicyListPage.ts:80 | ||||||
| #: src/pages/property-mappings/PropertyMappingListPage.ts:69 | #: src/pages/property-mappings/PropertyMappingListPage.ts:69 | ||||||
| #: src/pages/providers/ProviderListPage.ts:76 | #: src/pages/providers/ProviderListPage.ts:76 | ||||||
| #: src/pages/sources/SourcesListPage.ts:72 | #: src/pages/sources/SourcesListPage.ts:73 | ||||||
| #: src/pages/stages/StageListPage.ts:88 | #: src/pages/stages/StageListPage.ts:88 | ||||||
| #: src/pages/users/UserActiveForm.ts:41 | #: src/pages/users/UserActiveForm.ts:41 | ||||||
| msgid "Update {0}" | msgid "Update {0}" | ||||||
| @ -3734,7 +3769,7 @@ msgstr "" | |||||||
| msgid "Userinfo URL" | msgid "Userinfo URL" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/stages/identification/IdentificationStage.ts:142 | #: src/flows/stages/identification/IdentificationStage.ts:150 | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:78 | #: src/pages/stages/identification/IdentificationStageForm.ts:78 | ||||||
| #: src/pages/user-settings/UserDetailsPage.ts:57 | #: src/pages/user-settings/UserDetailsPage.ts:57 | ||||||
| #: src/pages/users/UserForm.ts:47 | #: src/pages/users/UserForm.ts:47 | ||||||
| @ -3865,6 +3900,10 @@ msgstr "" | |||||||
| msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." | msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/invitation/InvitationForm.ts:66 | ||||||
|  | msgid "When enabled, the invitation will be deleted after usage." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts:94 | #: src/pages/stages/identification/IdentificationStageForm.ts:94 | ||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3883,7 +3922,7 @@ msgstr "" | |||||||
| msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." | msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/flows/FlowExecutor.ts:134 | #: src/flows/FlowExecutor.ts:135 | ||||||
| msgid "Whoops!" | msgid "Whoops!" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ import { ifDefined } from "lit-html/directives/if-defined"; | |||||||
| import "./ldap/LDAPSourceForm"; | import "./ldap/LDAPSourceForm"; | ||||||
| import "./saml/SAMLSourceForm"; | import "./saml/SAMLSourceForm"; | ||||||
| import "./oauth/OAuthSourceForm"; | import "./oauth/OAuthSourceForm"; | ||||||
|  | import "./plex/PlexSourceForm"; | ||||||
|  |  | ||||||
| @customElement("ak-source-list") | @customElement("ak-source-list") | ||||||
| export class SourceListPage extends TablePage<Source> { | export class SourceListPage extends TablePage<Source> { | ||||||
|  | |||||||
							
								
								
									
										183
									
								
								web/src/pages/sources/plex/PlexSourceForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								web/src/pages/sources/plex/PlexSourceForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | |||||||
|  | import { PlexSource, SourcesApi, FlowsApi, FlowDesignationEnum } from "authentik-api"; | ||||||
|  | import { t } from "@lingui/macro"; | ||||||
|  | import { customElement, property } from "lit-element"; | ||||||
|  | import { html, TemplateResult } from "lit-html"; | ||||||
|  | import { DEFAULT_CONFIG } from "../../../api/Config"; | ||||||
|  | import { Form } from "../../../elements/forms/Form"; | ||||||
|  | import "../../../elements/forms/FormGroup"; | ||||||
|  | import "../../../elements/forms/HorizontalFormElement"; | ||||||
|  | import { ifDefined } from "lit-html/directives/if-defined"; | ||||||
|  | import { until } from "lit-html/directives/until"; | ||||||
|  | import { first, randomString } from "../../../utils"; | ||||||
|  | import { PlexAPIClient, PlexResource, popupCenterScreen} from "../../../flows/sources/plex/API"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @customElement("ak-source-plex-form") | ||||||
|  | export class PlexSourceForm extends Form<PlexSource> { | ||||||
|  |  | ||||||
|  |     set sourceSlug(value: string) { | ||||||
|  |         new SourcesApi(DEFAULT_CONFIG).sourcesPlexRead({ | ||||||
|  |             slug: value, | ||||||
|  |         }).then(source => { | ||||||
|  |             this.source = source; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @property({attribute: false}) | ||||||
|  |     source: PlexSource = { | ||||||
|  |         clientId: randomString(40) | ||||||
|  |     } as PlexSource; | ||||||
|  |  | ||||||
|  |     @property() | ||||||
|  |     plexToken?: string; | ||||||
|  |  | ||||||
|  |     @property({attribute: false}) | ||||||
|  |     plexResources?: PlexResource[]; | ||||||
|  |  | ||||||
|  |     getSuccessMessage(): string { | ||||||
|  |         if (this.source) { | ||||||
|  |             return t`Successfully updated source.`; | ||||||
|  |         } else { | ||||||
|  |             return t`Successfully created source.`; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     send = (data: PlexSource): Promise<PlexSource> => { | ||||||
|  |         if (this.source.slug) { | ||||||
|  |             return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({ | ||||||
|  |                 slug: this.source.slug, | ||||||
|  |                 data: data | ||||||
|  |             }); | ||||||
|  |         } else { | ||||||
|  |             return new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({ | ||||||
|  |                 data: data | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     async doAuth(): Promise<void> { | ||||||
|  |         const authInfo = await PlexAPIClient.getPin(this.source?.clientId); | ||||||
|  |         const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700); | ||||||
|  |         PlexAPIClient.pinPoll(this.source?.clientId || "", authInfo.pin.id).then(token => { | ||||||
|  |             authWindow?.close(); | ||||||
|  |             this.plexToken = token; | ||||||
|  |             this.loadServers(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async loadServers(): Promise<void> { | ||||||
|  |         if (!this.plexToken) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         this.plexResources = await new PlexAPIClient(this.plexToken).getServers(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     renderForm(): TemplateResult { | ||||||
|  |         return html`<form class="pf-c-form pf-m-horizontal"> | ||||||
|  |             <ak-form-element-horizontal | ||||||
|  |                 label=${t`Name`} | ||||||
|  |                 ?required=${true} | ||||||
|  |                 name="name"> | ||||||
|  |                 <input type="text" value="${ifDefined(this.source?.name)}" class="pf-c-form-control" required> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal | ||||||
|  |                 label=${t`Slug`} | ||||||
|  |                 ?required=${true} | ||||||
|  |                 name="slug"> | ||||||
|  |                 <input type="text" value="${ifDefined(this.source?.slug)}" class="pf-c-form-control" required> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal name="enabled"> | ||||||
|  |                 <div class="pf-c-check"> | ||||||
|  |                     <input type="checkbox" class="pf-c-check__input" ?checked=${first(this.source?.enabled, true)}> | ||||||
|  |                     <label class="pf-c-check__label"> | ||||||
|  |                         ${t`Enabled`} | ||||||
|  |                     </label> | ||||||
|  |                 </div> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |  | ||||||
|  |             <ak-form-group .expanded=${true}> | ||||||
|  |                 <span slot="header"> | ||||||
|  |                     ${t`Protocol settings`} | ||||||
|  |                 </span> | ||||||
|  |                 <div slot="body" class="pf-c-form"> | ||||||
|  |                     <ak-form-element-horizontal | ||||||
|  |                         label=${t`Client ID`} | ||||||
|  |                         ?required=${true} | ||||||
|  |                         name="clientId"> | ||||||
|  |                         <input type="text" value="${first(this.source?.clientId)}" class="pf-c-form-control" required> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                     <ak-form-element-horizontal | ||||||
|  |                         label=${t`Allowed servers`} | ||||||
|  |                         ?required=${true} | ||||||
|  |                         name="allowedServers"> | ||||||
|  |                         <select class="pf-c-form-control" multiple> | ||||||
|  |                             ${this.plexResources?.map(r => { | ||||||
|  |                                 const selected = Array.from(this.source?.allowedServers || []).some(server => { | ||||||
|  |                                     return server == r.clientIdentifier; | ||||||
|  |                                 }); | ||||||
|  |                                 return html`<option value=${r.clientIdentifier} ?selected=${selected}>${r.name}</option>`; | ||||||
|  |                             })} | ||||||
|  |                         </select> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`Select which server a user has to be a member of to be allowed to authenticate.`}</p> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p> | ||||||
|  |                         <p class="pf-c-form__helper-text"> | ||||||
|  |                             <button class="pf-c-button pf-m-primary" type="button" @click=${() => { | ||||||
|  |                                 this.doAuth(); | ||||||
|  |                             }}> | ||||||
|  |                                 ${t`Load servers`} | ||||||
|  |                             </button> | ||||||
|  |                         </p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                 </div> | ||||||
|  |             </ak-form-group> | ||||||
|  |             <ak-form-group> | ||||||
|  |                 <span slot="header"> | ||||||
|  |                     ${t`Flow settings`} | ||||||
|  |                 </span> | ||||||
|  |                 <div slot="body" class="pf-c-form"> | ||||||
|  |                     <ak-form-element-horizontal | ||||||
|  |                         label=${t`Authentication flow`} | ||||||
|  |                         ?required=${true} | ||||||
|  |                         name="authenticationFlow"> | ||||||
|  |                         <select class="pf-c-form-control"> | ||||||
|  |                             ${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||||
|  |                                 ordering: "pk", | ||||||
|  |                                 designation: FlowDesignationEnum.Authentication, | ||||||
|  |                             }).then(flows => { | ||||||
|  |                                 return flows.results.map(flow => { | ||||||
|  |                                     let selected = this.source?.authenticationFlow === flow.pk; | ||||||
|  |                                     if (!this.source?.pk && !this.source?.authenticationFlow && flow.slug === "default-source-authentication") { | ||||||
|  |                                         selected = true; | ||||||
|  |                                     } | ||||||
|  |                                     return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`; | ||||||
|  |                                 }); | ||||||
|  |                             }), html`<option>${t`Loading...`}</option>`)} | ||||||
|  |                         </select> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`Flow to use when authenticating existing users.`}</p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                     <ak-form-element-horizontal | ||||||
|  |                         label=${t`Enrollment flow`} | ||||||
|  |                         ?required=${true} | ||||||
|  |                         name="enrollmentFlow"> | ||||||
|  |                         <select class="pf-c-form-control"> | ||||||
|  |                             ${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ | ||||||
|  |                                 ordering: "pk", | ||||||
|  |                                 designation: FlowDesignationEnum.Enrollment, | ||||||
|  |                             }).then(flows => { | ||||||
|  |                                 return flows.results.map(flow => { | ||||||
|  |                                     let selected = this.source?.enrollmentFlow === flow.pk; | ||||||
|  |                                     if (!this.source?.pk && !this.source?.enrollmentFlow && flow.slug === "default-source-enrollment") { | ||||||
|  |                                         selected = true; | ||||||
|  |                                     } | ||||||
|  |                                     return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`; | ||||||
|  |                                 }); | ||||||
|  |                             }), html`<option>${t`Loading...`}</option>`)} | ||||||
|  |                         </select> | ||||||
|  |                         <p class="pf-c-form__helper-text">${t`Flow to use when enrolling new users.`}</p> | ||||||
|  |                     </ak-form-element-horizontal> | ||||||
|  |                 </div> | ||||||
|  |             </ak-form-group> | ||||||
|  |         </form>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @ -51,6 +51,17 @@ export class InvitationForm extends Form<Invitation> { | |||||||
|                 </ak-codemirror> |                 </ak-codemirror> | ||||||
|                 <p class="pf-c-form__helper-text">${t`Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.`}</p> |                 <p class="pf-c-form__helper-text">${t`Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.`}</p> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal name="singleUse"> | ||||||
|  |                 <div class="pf-c-check"> | ||||||
|  |                     <input type="checkbox" class="pf-c-check__input" ?checked=${first(this.invitation?.singleUse, true)}> | ||||||
|  |                     <label class="pf-c-check__label"> | ||||||
|  |                         ${t`Single use`} | ||||||
|  |                     </label> | ||||||
|  |                 </div> | ||||||
|  |                 <p class="pf-c-form__helper-text"> | ||||||
|  |                     ${t`When enabled, the invitation will be deleted after usage.`} | ||||||
|  |                 </p> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|         </form>`; |         </form>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								website/docs/releases/next.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								website/docs/releases/next.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | --- | ||||||
|  | title: Next | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Headline Changes | ||||||
|  |  | ||||||
|  | - Compatibility with forwardAuth/auth_request | ||||||
|  |  | ||||||
|  |     The authentik proxy is now compatible with forwardAuth (traefik) / auth_request (nginx). All that is required is the latest version of the outpost, | ||||||
|  |     and the correct config from [here](../outposts/proxy.mdx). | ||||||
|  |  | ||||||
|  | - Docker images for ARM | ||||||
|  |  | ||||||
|  |     Docker images are now built for amd64, arm64, arm v7 and arm v8. | ||||||
|  |  | ||||||
|  | - Deprecated Group membership has been removed. | ||||||
|  |  | ||||||
|  | ## Minor changes | ||||||
|  |  | ||||||
|  | - Improved compatibility of the flow interface with password managers. | ||||||
|  |  | ||||||
|  | ## Upgrading | ||||||
|  |  | ||||||
|  | This release does not introduce any new requirements. | ||||||
|  |  | ||||||
|  | ### docker-compose | ||||||
|  |  | ||||||
|  | Download the latest docker-compose file from [here](https://raw.githubusercontent.com/goauthentik/authentik/version-2021.4/docker-compose.yml). Afterwards, simply run `docker-compose up -d` and then the standard upgrade command of `docker-compose run --rm server migrate`. | ||||||
|  |  | ||||||
|  | ### Kubernetes | ||||||
|  |  | ||||||
|  | Run `helm repo update` and then upgrade your release with `helm upgrade authentik authentik/authentik --devel -f values.yaml`. | ||||||
| @ -68,7 +68,7 @@ postgresql: | |||||||
| Afterwards you can upgrade as usual from the new repository: | Afterwards you can upgrade as usual from the new repository: | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| helm upgrade passbook authentik/authentik --devel -f values.yaml | helm upgrade authentik authentik/authentik --devel -f values.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Post-upgrade notes | ## Post-upgrade notes | ||||||
|  | |||||||
| @ -55,4 +55,4 @@ Download the latest docker-compose file from [here](https://raw.githubuserconten | |||||||
|  |  | ||||||
| ### Kubernetes | ### Kubernetes | ||||||
|  |  | ||||||
| Run `helm repo update` and then upgrade your release with `helm upgrade passbook authentik/authentik --devel -f values.yaml`. | Run `helm repo update` and then upgrade your release with `helm upgrade authentik authentik/authentik --devel -f values.yaml`. | ||||||
|  | |||||||
| @ -64,4 +64,4 @@ Download the latest docker-compose file from [here](https://raw.githubuserconten | |||||||
|  |  | ||||||
| ### Kubernetes | ### Kubernetes | ||||||
|  |  | ||||||
| Run `helm repo update` and then upgrade your release with `helm upgrade passbook authentik/authentik --devel -f values.yaml`. | Run `helm repo update` and then upgrade your release with `helm upgrade authentik authentik/authentik --devel -f values.yaml`. | ||||||
|  | |||||||
| @ -128,4 +128,4 @@ Download the latest docker-compose file from [here](https://raw.githubuserconten | |||||||
|  |  | ||||||
| ### Kubernetes | ### Kubernetes | ||||||
|  |  | ||||||
| Run `helm repo update` and then upgrade your release with `helm upgrade passbook authentik/authentik --devel -f values.yaml`. | Run `helm repo update` and then upgrade your release with `helm upgrade authentik authentik/authentik --devel -f values.yaml`. | ||||||
|  | |||||||
| @ -91,4 +91,4 @@ Download the latest docker-compose file from [here](https://raw.githubuserconten | |||||||
|  |  | ||||||
| ### Kubernetes | ### Kubernetes | ||||||
|  |  | ||||||
| Run `helm repo update` and then upgrade your release with `helm upgrade passbook authentik/authentik --devel -f values.yaml`. | Run `helm repo update` and then upgrade your release with `helm upgrade authentik authentik/authentik --devel -f values.yaml`. | ||||||
|  | |||||||
| @ -138,4 +138,4 @@ Download the latest docker-compose file from [here](https://raw.githubuserconten | |||||||
|  |  | ||||||
| ### Kubernetes | ### Kubernetes | ||||||
|  |  | ||||||
| Run `helm repo update` and then upgrade your release with `helm upgrade passbook authentik/authentik --devel -f values.yaml`. | Run `helm repo update` and then upgrade your release with `helm upgrade authentik authentik/authentik --devel -f values.yaml`. | ||||||
|  | |||||||
							
								
								
									
										6287
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6287
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -11,7 +11,7 @@ | |||||||
|         "serve": "docusaurus serve" |         "serve": "docusaurus serve" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@docusaurus/preset-classic": "2.0.0-alpha.74", |         "@docusaurus/preset-classic": "2.0.0-alpha.75", | ||||||
|         "@mdx-js/react": "^1.6.22", |         "@mdx-js/react": "^1.6.22", | ||||||
|         "clsx": "^1.1.1", |         "clsx": "^1.1.1", | ||||||
|         "postcss": "^8.2.13", |         "postcss": "^8.2.13", | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer