Compare commits
160 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
e20bb7d636 | |||
e9abc25b92 | |||
dc930c0cdf | |||
464a1c0536 | |||
837d2f6fab | |||
8f00d73512 | |||
b75feab709 | |||
9c8433ec4d | |||
ef080900a4 | |||
10b45a8dea | |||
c43ac1f704 | |||
14d702450a | |||
0a1a2a035e | |||
ace777ebbe | |||
8a6879afa5 | |||
fdc7f14056 | |||
8be80aaf9d | |||
e476f2dda2 | |||
5d48cfab14 | |||
1f22f0e7bb | |||
ce082ead5e | |||
dd2cd09637 | |||
828fe07fca | |||
a074ea70e9 | |||
84ce2c1df2 | |||
8628595590 | |||
7b8e5c4272 | |||
caa5dc1d14 | |||
f328b21e89 | |||
52abd959eb | |||
a0cd17a257 | |||
32c5bf04b8 | |||
766c4873a0 | |||
240136154b | |||
78dd7b0341 | |||
0021a93952 | |||
67240fb9ad | |||
4add0bbe86 | |||
d2dd7d1366 | |||
476e57daa2 | |||
4eb8a0dcd1 | |||
60615c9f3e | |||
b5b8573d87 | |||
2e44c1cdfc | |||
31909a4d78 | |||
4a444e667a | |||
f67b57e369 | |||
6be19962d2 | |||
262a9fa2a0 | |||
e8ba159756 | |||
0b03d66a2f | |||
7c858c9626 | |||
71b6839d03 | |||
ada49c077a | |||
7880c7fb98 | |||
2b48ba4103 | |||
5e67f68f2b | |||
1992b89154 | |||
9ab2088ab7 | |||
a9d0d96418 | |||
c476503594 | |||
de74f3ec1f | |||
ce98255607 | |||
53b9e5b93f | |||
7aeb390eac | |||
5df9ad63cf | |||
e4400476a2 | |||
ef3c01ec34 | |||
b136d3bc69 | |||
c34fcc73dc | |||
11b09c4ebd | |||
e32070ddeb | |||
33a8cea007 | |||
d01fd7cdb7 | |||
1770e42cbf | |||
2fed739be7 | |||
aa820b2b4d | |||
582d2eb5eb | |||
c5e2635903 | |||
cfe0a7a694 | |||
c579540473 | |||
35f2b06611 | |||
9c4f025d71 | |||
d8b8e8a5a3 | |||
ec34c3eb75 | |||
0554c94c53 | |||
19a663a645 | |||
e72881b2a9 | |||
4452ff171e | |||
39bdc3a9a9 | |||
33bb6edf8c | |||
2eb18ff5e6 | |||
aeb1b5e8f2 | |||
bd8447d5a7 | |||
35fad191b8 | |||
40a6f15cf1 | |||
420465981b | |||
4f9f936a7f | |||
85c9fbe763 | |||
3d9874be69 | |||
9742d19729 | |||
5a25e6d697 | |||
7798a046db | |||
7a562fe8c0 | |||
6821679fbc | |||
513d3c1c31 | |||
30cb468ec5 | |||
8b66fa55a6 | |||
55bb9b6643 | |||
1b79fad6cf | |||
f9976492e7 | |||
2fd0e46378 | |||
fd0ad20031 | |||
13b75c15f0 | |||
d329995740 | |||
cd1b0c67ea | |||
ab7941922f | |||
e057d5fe0a | |||
3fb53e8311 | |||
96b9d931f3 | |||
a35f77c612 | |||
f287745c53 | |||
65e09f92cd | |||
9b6446701e | |||
71f7e23fe4 | |||
59eb89db6c | |||
939b55ce29 | |||
7ba4e63c47 | |||
fae92f6bc8 | |||
f9bf491240 | |||
4f27a97e10 | |||
a0daaabfde | |||
ea7ecb50c0 | |||
e7626d0716 | |||
e9d29b956d | |||
4a4ee98dec | |||
0d0baaa2f9 | |||
1be1654bf2 | |||
ca51afb7df | |||
11c8ae8f18 | |||
858fcb8554 | |||
571772854b | |||
c91b40fc07 | |||
a736e708ae | |||
5c133a6c30 | |||
078dfb30f3 | |||
b526250515 | |||
e52d397cb7 | |||
633029be3f | |||
4147fbb839 | |||
430e3c576c | |||
d6f60ad9ec | |||
de6f663688 | |||
fe17c3aa34 | |||
07b2525278 | |||
9f758d19ba | |||
4216577565 | |||
f3396226e8 | |||
ae7959ff51 | |||
b42b7be726 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.4.1-rc1
|
||||
current_version = 2021.4.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/authentik:2021.4.1-rc1
|
||||
-t beryju/authentik:2021.4.3
|
||||
-t beryju/authentik:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik:2021.4.1-rc1
|
||||
run: docker push beryju/authentik:2021.4.3
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd outpost/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-proxy:2021.4.1-rc1 \
|
||||
-t beryju/authentik-proxy:2021.4.3 \
|
||||
-t beryju/authentik-proxy:latest \
|
||||
-f proxy.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-proxy:2021.4.1-rc1
|
||||
run: docker push beryju/authentik-proxy:2021.4.3
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-proxy:latest
|
||||
build-static:
|
||||
@ -72,11 +72,11 @@ jobs:
|
||||
cd web/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-static:2021.4.1-rc1 \
|
||||
-t beryju/authentik-static:2021.4.3 \
|
||||
-t beryju/authentik-static:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-static:2021.4.1-rc1
|
||||
run: docker push beryju/authentik-static:2021.4.3
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-static:latest
|
||||
test-release:
|
||||
@ -110,5 +110,5 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 2021.4.1-rc1
|
||||
tagName: 2021.4.3
|
||||
environment: beryjuorg-prod
|
||||
|
207
Pipfile.lock
generated
207
Pipfile.lock
generated
@ -116,17 +116,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:e86c15049dc07cb67e8b466795f004f1f23c1acf078d47283cd5e4a692a5aa37"
|
||||
"sha256:1d26f6e7ae3c940cb07119077ac42485dcf99164350da0ab50d0f5ad345800cd",
|
||||
"sha256:3bf3305571f3c8b738a53e9e7dcff59137dffe94670046c084a17f9fa4599ff3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.48"
|
||||
"version": "==1.17.53"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:99c4f96bffae406f6dec1fa94e0886993e5bcd8657604d950abb43a49121fa7c",
|
||||
"sha256:af27a8133f5b8f6a55738ccd5efd35689f1822cfd447625a839ef7a991eab64f"
|
||||
"sha256:d5e70d17b91c9b5867be7d6de0caa7dde9ed789bed62f03ea9b60718dc9350bf",
|
||||
"sha256:e303500c4e80f6a706602da53daa6f751cfa8f491665c99a24ee732ab6321573"
|
||||
],
|
||||
"version": "==1.20.48"
|
||||
"version": "==1.20.53"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -436,10 +437,10 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:186fe2564634d67fbbb64f3daf8bc8c9cecbb2a7f535ed1a8a71795e50db8d87",
|
||||
"sha256:70b39558712826e41f65e5f05a8d879361deaf84df8883e5dd0ec3d0da6ab66e"
|
||||
"sha256:010f011c4e27d3d5eb01106fba6aac39d164842dfcd8709955c4638f5b11ccf8",
|
||||
"sha256:f30a672a64d91cc2e3137765d088c5deec26416246f7a9e956eaf69a8d7ed49c"
|
||||
],
|
||||
"version": "==1.28.1"
|
||||
"version": "==1.29.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -1105,10 +1106,10 @@
|
||||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2",
|
||||
"sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547"
|
||||
"sha256:35627b86af8ff97e7ac27975fe0a98a312814b46c6333d8a6b889627bcd80994",
|
||||
"sha256:efa5bd92a897b6a8d5c1383828dca3d52d0790e0756d49740563a3fb6ed03246"
|
||||
],
|
||||
"version": "==0.3.6"
|
||||
"version": "==0.3.7"
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
@ -1373,59 +1374,59 @@
|
||||
},
|
||||
"zope.interface": {
|
||||
"hashes": [
|
||||
"sha256:02d3535aa18e34ce97c58d241120b7554f7d1cf4f8002fc9675cc7e7745d20e8",
|
||||
"sha256:0378a42ec284b65706d9ef867600a4a31701a0d6773434e6537cfc744e3343f4",
|
||||
"sha256:07d289358a8c565ea09e426590dd1179f93cf5ac3dd17d43fcc4fc63c1a9d275",
|
||||
"sha256:0e6cdbdd94ae94d1433ab51f46a76df0f2cd041747c31baec1c1ffa4e76bd0c1",
|
||||
"sha256:11354fb8b8bdc5cdd66358ed4f1f0ce739d78ff6d215d33b8f3ae282258c0f11",
|
||||
"sha256:12588a46ae0a99f172c4524cbbc3bb870f32e0f8405e9fa11a5ef3fa3a808ad7",
|
||||
"sha256:16caa44a06f6b0b2f7626ced4b193c1ae5d09c1b49c9b4962c93ae8aa2134f55",
|
||||
"sha256:18c478b89b6505756f007dcf76a67224a23dcf0f365427742ed0c0473099caa4",
|
||||
"sha256:221b41442cf4428fcda7fc958c9721c916709e2a3a9f584edd70f1493a09a762",
|
||||
"sha256:26109c50ccbcc10f651f76277cfc05fba8418a907daccc300c9247f24b3158a2",
|
||||
"sha256:28d8157f8c77662a1e0796a7d3cfa8910289131d4b4dd4e10b2686ab1309b67b",
|
||||
"sha256:2c51689b7b40c7d9c7e8a678350e73dc647945a13b4e416e7a02bbf0c37bdb01",
|
||||
"sha256:2ec58e1e1691dde4fbbd97f8610de0f8f1b1a38593653f7d3b8e931b9cd6d67f",
|
||||
"sha256:416feb6500f7b6fc00d32271f6b8495e67188cb5eb51fc8e289b81fdf465a9cb",
|
||||
"sha256:520352b18adea5478bbf387e9c77910a914985671fe36bc5ef19fdcb67a854bc",
|
||||
"sha256:527415b5ca201b4add44026f70278fbc0b942cf0801a26ca5527cb0389b6151e",
|
||||
"sha256:54243053316b5eec92affe43bbace7c8cd946bc0974a4aa39ff1371df0677b22",
|
||||
"sha256:61b8454190b9cc87279232b6de28dee0bad040df879064bb2f0e505cda907918",
|
||||
"sha256:672668729edcba0f2ee522ab177fcad91c81cfce991c24d8767765e2637d3515",
|
||||
"sha256:67aa26097e194947d29f2b5a123830e03da1519bcce10cac034a51fcdb99c34f",
|
||||
"sha256:6e7305e42b5f54e5ccf51820de46f0a7c951ba7cb9e3f519e908545b0f5628d0",
|
||||
"sha256:7234ac6782ca43617de803735949f79b894f0c5d353fbc001d745503c69e6d1d",
|
||||
"sha256:7426bea25bdf92f00fa52c7b30fcd2a2f71c21cf007178971b1f248b6c2d3145",
|
||||
"sha256:74b331c5d5efdddf5bbd9e1f7d8cb91a0d6b9c4ba45ca3e9003047a84dca1a3b",
|
||||
"sha256:79b6db1a18253db86e9bf1e99fa829d60fd3fc7ac04f4451c44e4bdcf6756a42",
|
||||
"sha256:7d79cd354ae0a033ac7b86a2889c9e8bb0bb48243a6ed27fc5064ce49b842ada",
|
||||
"sha256:823d1b4a6a028b8327e64865e2c81a8959ae9f4e7c9c8e0eec814f4f9b36b362",
|
||||
"sha256:8715717a5861932b7fe7f3cbd498c82ff4132763e2fea182cc95e53850394ec1",
|
||||
"sha256:89a6091f2d07936c8a96ce56f2000ecbef20fb420a94845e7d53913c558a6378",
|
||||
"sha256:8af4b3116e4a37059bc8c7fe36d4a73d7c1d8802a1d8b6e549f1380d13a40160",
|
||||
"sha256:8b4b0034e6c7f30133fa64a1cc276f8f1a155ef9529e7eb93a3c1728b40c0f5c",
|
||||
"sha256:92195df3913c1de80062635bf64cd7bd0d0934a7fa1689b6d287d1cbbd16922c",
|
||||
"sha256:96c2e68385f3848d58f19b2975a675532abdb65c8fa5f04d94b95b27b6b1ffa7",
|
||||
"sha256:9c7044dbbf8c58420a9ef4ed6901f5a8b7698d90cd984d7f57a18c78474686f6",
|
||||
"sha256:a1937efed7e3fe0ee74630e1960df887d8aa83c571e1cf4db9d15b9c181d457d",
|
||||
"sha256:a38c10423a475a1658e2cb8f52cf84ec20a4c0adff724dd43a6b45183f499bc1",
|
||||
"sha256:a413c424199bcbab71bf5fa7538246f27177fbd6dd74b2d9c5f34878658807f8",
|
||||
"sha256:b18a855f8504743e0a2d8b75d008c7720d44e4c76687e13f959e35d9a13eb397",
|
||||
"sha256:b4d59ab3608538e550a72cea13d3c209dd72b6e19e832688da7884081c01594e",
|
||||
"sha256:b51d3f1cd87f488455f43046d72003689024b0fa9b2d53635db7523033b19996",
|
||||
"sha256:c02105deda867d09cdd5088d08708f06d75759df6f83d8f7007b06f422908a30",
|
||||
"sha256:c7b6032dc4490b0dcaf078f09f5b382dc35493cb7f473840368bf0de3196c2b6",
|
||||
"sha256:c95b355dba2aaf5177dff943b25ded0529a7feb80021d5fdb114a99f0a1ef508",
|
||||
"sha256:c980ae87863d76b1ea9a073d6d95554b4135032d34bc541be50c07d4a085821b",
|
||||
"sha256:d12895cd083e35e9e032eb4b57645b91116f8979527381a8d864d1f6b8cb4a2e",
|
||||
"sha256:d3cd9bad547a8e5fbe712a1dc1413aff1b917e8d39a2cd1389a6f933b7a21460",
|
||||
"sha256:e8809b01f27f679e3023b9e2013051e0a3f17abff4228cb5197663afd8a0f2c7",
|
||||
"sha256:f3c37b0dc1898e305aad4f7a1d75f6da83036588c28a9ce0afc681ff5245a601",
|
||||
"sha256:f966765f54b536e791541458de84a737a6adba8467190f17a8fe7f85354ba908",
|
||||
"sha256:fa939c2e2468142c9773443d4038e7c915b0cc1b670d3c9192bdc503f7ea73e9",
|
||||
"sha256:fcc5c1f95102989d2e116ffc8467963554ce89f30a65a3ea86a4d06849c498d8"
|
||||
"sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192",
|
||||
"sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702",
|
||||
"sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09",
|
||||
"sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4",
|
||||
"sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a",
|
||||
"sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3",
|
||||
"sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf",
|
||||
"sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c",
|
||||
"sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d",
|
||||
"sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78",
|
||||
"sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83",
|
||||
"sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531",
|
||||
"sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46",
|
||||
"sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021",
|
||||
"sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94",
|
||||
"sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc",
|
||||
"sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63",
|
||||
"sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54",
|
||||
"sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117",
|
||||
"sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25",
|
||||
"sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05",
|
||||
"sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e",
|
||||
"sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1",
|
||||
"sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004",
|
||||
"sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2",
|
||||
"sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e",
|
||||
"sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f",
|
||||
"sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f",
|
||||
"sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120",
|
||||
"sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f",
|
||||
"sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1",
|
||||
"sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9",
|
||||
"sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e",
|
||||
"sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7",
|
||||
"sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8",
|
||||
"sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b",
|
||||
"sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155",
|
||||
"sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7",
|
||||
"sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c",
|
||||
"sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325",
|
||||
"sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d",
|
||||
"sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb",
|
||||
"sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e",
|
||||
"sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959",
|
||||
"sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7",
|
||||
"sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920",
|
||||
"sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e",
|
||||
"sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48",
|
||||
"sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8",
|
||||
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
|
||||
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
|
||||
],
|
||||
"version": "==5.3.0"
|
||||
"version": "==5.4.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -1438,10 +1439,10 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:6b0ed1af831570e500e2437625979eaa3b36011f66ddfc4ce930128610258ca9",
|
||||
"sha256:cd80bf957c49765dce6d92c43163ff9d2abc43132ce64d4b1b47717c6d2522df"
|
||||
"sha256:ad63b8552c70939568966811a088ef0bc880f99a24a00834abd0e3681b514f91",
|
||||
"sha256:bea3f32799fbb8581f58431c12591bc20ce11cbc90ad82e2ea5717d94f2080d5"
|
||||
],
|
||||
"version": "==2.5.2"
|
||||
"version": "==2.5.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@ -1661,11 +1662,11 @@
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
"sha256:355dddb25ef07dbdb77a818b0860ada722aab654c24da34aab916ec26d6390ba",
|
||||
"sha256:f8d77f7da47a7019cda5cb669c214f03033208f9e945094661299d2637c0da06"
|
||||
"sha256:a5a4515209a6237d1d390a4a307d53f53baaf4f058ecf4bb556c775d208f6b0d",
|
||||
"sha256:dc5ed27bb7662d73444ccd15a0b3964ed6ced6cc2712b85db616102062d2ec35"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4.2"
|
||||
"version": "==2.4.3"
|
||||
},
|
||||
"pylint-plugin-utils": {
|
||||
"hashes": [
|
||||
@ -1691,11 +1692,11 @@
|
||||
},
|
||||
"pytest-django": {
|
||||
"hashes": [
|
||||
"sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2",
|
||||
"sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f"
|
||||
"sha256:80f8875226ec4dc0b205f0578072034563879d98d9b1bec143a80b9045716cb0",
|
||||
"sha256:a51150d8962200250e850c6adcab670779b9c2aa07271471059d1fb92a843fa9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.1.0"
|
||||
"version": "==4.2.0"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@ -1816,38 +1817,38 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1",
|
||||
"sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d",
|
||||
"sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6",
|
||||
"sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd",
|
||||
"sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37",
|
||||
"sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151",
|
||||
"sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07",
|
||||
"sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440",
|
||||
"sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70",
|
||||
"sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496",
|
||||
"sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea",
|
||||
"sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400",
|
||||
"sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc",
|
||||
"sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606",
|
||||
"sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc",
|
||||
"sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581",
|
||||
"sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412",
|
||||
"sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a",
|
||||
"sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2",
|
||||
"sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787",
|
||||
"sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f",
|
||||
"sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937",
|
||||
"sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64",
|
||||
"sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487",
|
||||
"sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b",
|
||||
"sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41",
|
||||
"sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a",
|
||||
"sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3",
|
||||
"sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166",
|
||||
"sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"
|
||||
"sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace",
|
||||
"sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff",
|
||||
"sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266",
|
||||
"sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528",
|
||||
"sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6",
|
||||
"sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808",
|
||||
"sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4",
|
||||
"sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363",
|
||||
"sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341",
|
||||
"sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04",
|
||||
"sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41",
|
||||
"sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e",
|
||||
"sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3",
|
||||
"sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899",
|
||||
"sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805",
|
||||
"sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c",
|
||||
"sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c",
|
||||
"sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39",
|
||||
"sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a",
|
||||
"sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3",
|
||||
"sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7",
|
||||
"sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f",
|
||||
"sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075",
|
||||
"sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0",
|
||||
"sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40",
|
||||
"sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428",
|
||||
"sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927",
|
||||
"sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3",
|
||||
"sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f",
|
||||
"sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
[](https://discord.gg/KPnmtNWy)
|
||||
[](https://discord.gg/jg33eMhnj6)
|
||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=1)
|
||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=1)
|
||||
[](https://codecov.io/gh/BeryJu/authentik)
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.4.1-rc1"
|
||||
__version__ = "2021.4.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -6,7 +6,7 @@ from drf_yasg.utils import swagger_auto_schema
|
||||
from packaging.version import parse
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
@ -50,7 +50,9 @@ class VersionSerializer(PassiveSerializer):
|
||||
class VersionViewSet(ListModelMixin, GenericViewSet):
|
||||
"""Get running and latest version."""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = None
|
||||
filter_backends = []
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return None
|
||||
|
@ -1,61 +1,65 @@
|
||||
"""API Authentication"""
|
||||
from base64 import b64decode
|
||||
from base64 import b64decode, b64encode
|
||||
from binascii import Error
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
|
||||
LOGGER = get_logger()
|
||||
X_AUTHENTIK_PREVENT_BASIC_HEADER = "HTTP_X_AUTHENTIK_PREVENT_BASIC"
|
||||
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def token_from_header(raw_header: bytes) -> Optional[Token]:
|
||||
"""raw_header in the Format of `Basic dGVzdDp0ZXN0`"""
|
||||
"""raw_header in the Format of `Bearer dGVzdDp0ZXN0`"""
|
||||
auth_credentials = raw_header.decode()
|
||||
# Accept headers with Type format and without
|
||||
if " " in auth_credentials:
|
||||
auth_type, auth_credentials = auth_credentials.split()
|
||||
if auth_type.lower() != "basic":
|
||||
LOGGER.debug(
|
||||
"Unsupported authentication type, denying", type=auth_type.lower()
|
||||
)
|
||||
return None
|
||||
try:
|
||||
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
||||
except (UnicodeDecodeError, Error):
|
||||
if auth_credentials == "":
|
||||
return None
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
_, password = auth_credentials.split(":")
|
||||
else:
|
||||
password = auth_credentials
|
||||
# Legacy, accept basic auth thats fully encoded (2021.3 outposts)
|
||||
if " " not in auth_credentials:
|
||||
try:
|
||||
plain = b64decode(auth_credentials.encode()).decode()
|
||||
auth_type, body = plain.split()
|
||||
auth_credentials = f"{auth_type} {b64encode(body.encode()).decode()}"
|
||||
except (UnicodeDecodeError, Error):
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
auth_type, auth_credentials = auth_credentials.split()
|
||||
if auth_type.lower() not in ["basic", "bearer"]:
|
||||
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
||||
raise AuthenticationFailed("Unsupported authentication type")
|
||||
password = auth_credentials
|
||||
if auth_type.lower() == "basic":
|
||||
try:
|
||||
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
||||
except (UnicodeDecodeError, Error):
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
_, password = auth_credentials.split(":")
|
||||
else:
|
||||
password = auth_credentials
|
||||
if password == "": # nosec
|
||||
return None
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
||||
if not tokens.exists():
|
||||
LOGGER.debug("Token not found")
|
||||
return None
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
return tokens.first()
|
||||
|
||||
|
||||
class AuthentikTokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Basic authentication"""
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
|
||||
def authenticate(self, request: Request) -> Union[tuple[User, Any], None]:
|
||||
"""Token-based authentication using HTTP Basic authentication"""
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
auth = get_authorization_header(request)
|
||||
|
||||
token = token_from_header(auth)
|
||||
# None is only returned when the header isn't set.
|
||||
if not token:
|
||||
return None
|
||||
|
||||
return (token.user, None)
|
||||
|
||||
def authenticate_header(self, request: Request) -> str:
|
||||
if X_AUTHENTIK_PREVENT_BASIC_HEADER in request._request.META:
|
||||
return ""
|
||||
return 'Basic realm="authentik"'
|
||||
|
@ -11,6 +11,29 @@ authentik API Browser
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<script>
|
||||
function getCookie(name) {
|
||||
let cookieValue = "";
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
const rapidocEl = document.querySelector('rapi-doc');
|
||||
rapidocEl.addEventListener('before-try', (e) => {
|
||||
e.detail.request.headers.append('X-CSRFToken', getCookie("authentik_csrf"));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<rapi-doc
|
||||
spec-url="{{ path }}"
|
||||
heading-text="authentik"
|
||||
|
@ -3,6 +3,7 @@ from base64 import b64encode
|
||||
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from authentik.api.auth import token_from_header
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
@ -11,7 +12,7 @@ from authentik.core.models import Token, TokenIntents
|
||||
class TestAPIAuth(TestCase):
|
||||
"""Test API Authentication"""
|
||||
|
||||
def test_valid(self):
|
||||
def test_valid_basic(self):
|
||||
"""Test valid token"""
|
||||
token = Token.objects.create(
|
||||
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
|
||||
@ -19,19 +20,30 @@ class TestAPIAuth(TestCase):
|
||||
auth = b64encode(f":{token.key}".encode()).decode()
|
||||
self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
|
||||
|
||||
def test_valid_bearer(self):
|
||||
"""Test valid token"""
|
||||
token = Token.objects.create(
|
||||
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
|
||||
)
|
||||
self.assertEqual(token_from_header(f"Bearer {token.key}".encode()), token)
|
||||
|
||||
def test_invalid_type(self):
|
||||
"""Test invalid type"""
|
||||
self.assertIsNone(token_from_header("foo bar".encode()))
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
token_from_header("foo bar".encode())
|
||||
|
||||
def test_invalid_decode(self):
|
||||
"""Test invalid bas64"""
|
||||
self.assertIsNone(token_from_header("Basic bar".encode()))
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
token_from_header("Basic bar".encode())
|
||||
|
||||
def test_invalid_empty_password(self):
|
||||
"""Test invalid with empty password"""
|
||||
self.assertIsNone(token_from_header("Basic :".encode()))
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
token_from_header("Basic :".encode())
|
||||
|
||||
def test_invalid_no_token(self):
|
||||
"""Test invalid with no token"""
|
||||
auth = b64encode(":abc".encode()).decode()
|
||||
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
auth = b64encode(":abc".encode()).decode()
|
||||
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
|
||||
|
@ -113,6 +113,7 @@ router.register("core/user_consent", UserConsentViewSet)
|
||||
router.register("core/tokens", TokenViewSet)
|
||||
|
||||
router.register("outposts/outposts", OutpostViewSet)
|
||||
router.register("outposts/instances", OutpostViewSet)
|
||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||
router.register(
|
||||
|
@ -91,6 +91,15 @@ class ApplicationViewSet(ModelViewSet):
|
||||
applications.append(application)
|
||||
return applications
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
name="superuser_full_list",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
)
|
||||
]
|
||||
)
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Custom list method that checks Policy based access instead of guardian"""
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
@ -98,6 +107,13 @@ class ApplicationViewSet(ModelViewSet):
|
||||
|
||||
should_cache = request.GET.get("search", "") == ""
|
||||
|
||||
superuser_full_list = (
|
||||
str(request.GET.get("superuser_full_list", "false")).lower() == "true"
|
||||
)
|
||||
if superuser_full_list and request.user.is_superuser:
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
allowed_applications = []
|
||||
if not should_cache:
|
||||
allowed_applications = self._get_allowed_applications(queryset)
|
||||
|
@ -1,13 +1,17 @@
|
||||
"""Groups API Viewset"""
|
||||
from rest_framework.fields import JSONField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import is_dict
|
||||
from authentik.core.models import Group
|
||||
|
||||
|
||||
class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONField(validators=[is_dict], required=False)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""PropertyMapping API Views"""
|
||||
from json import dumps
|
||||
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework import mixins
|
||||
@ -91,7 +92,9 @@ class PropertyMappingViewSet(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass.component,
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
@ -100,6 +103,13 @@ class PropertyMappingViewSet(
|
||||
@swagger_auto_schema(
|
||||
request_body=PolicyTestSerializer(),
|
||||
responses={200: PropertyMappingTestResultSerializer, 400: "Invalid parameters"},
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
name="format_result",
|
||||
in_=openapi.IN_QUERY,
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
)
|
||||
],
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
# pylint: disable=unused-argument, invalid-name
|
||||
@ -110,6 +120,8 @@ class PropertyMappingViewSet(
|
||||
if not test_params.is_valid():
|
||||
return Response(test_params.errors, status=400)
|
||||
|
||||
format_result = str(request.GET.get("format_result", "false")).lower() == "true"
|
||||
|
||||
# User permission check, only allow mapping testing for users that are readable
|
||||
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
|
||||
pk=test_params.validated_data["user"].pk
|
||||
@ -124,7 +136,9 @@ class PropertyMappingViewSet(
|
||||
self.request,
|
||||
**test_params.validated_data.get("context", {}),
|
||||
)
|
||||
response_data["result"] = dumps(result)
|
||||
response_data["result"] = dumps(
|
||||
result, indent=(4 if format_result else None)
|
||||
)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
response_data["result"] = str(exc)
|
||||
response_data["successful"] = False
|
||||
|
@ -78,6 +78,7 @@ class ProviderViewSet(
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
data.append(
|
||||
@ -85,6 +86,7 @@ class ProviderViewSet(
|
||||
"name": _("SAML Provider from Metadata"),
|
||||
"description": _("Create a SAML Provider by importing its Metadata."),
|
||||
"component": "ak-provider-saml-import-form",
|
||||
"model_name": "",
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -81,6 +81,7 @@ class SourceViewSet(
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -11,7 +11,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Token
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.managed.api import ManagedSerializer
|
||||
|
||||
@ -64,7 +64,7 @@ class TokenViewSet(ModelViewSet):
|
||||
ordering = ["expires"]
|
||||
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
serializer.save(user=self.request.user)
|
||||
serializer.save(user=self.request.user, intent=TokenIntents.INTENT_API)
|
||||
|
||||
@permission_required("authentik_core.view_token_key")
|
||||
@swagger_auto_schema(
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""User API Views"""
|
||||
from django.http.response import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
||||
from guardian.utils import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, SerializerMethodField
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||
@ -12,13 +13,14 @@ from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||
from authentik.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
@ -26,6 +28,7 @@ class UserSerializer(ModelSerializer):
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
attributes = JSONField(validators=[is_dict], required=False)
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -120,12 +123,16 @@ class UserViewSet(ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@swagger_auto_schema(
|
||||
responses={"200": LinkSerializer(many=False)},
|
||||
responses={"200": LinkSerializer(many=False), "404": "No recovery flow found."},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def recovery(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = Flow.with_policy(request, designation=FlowDesignation.RECOVERY)
|
||||
if not flow:
|
||||
raise Http404
|
||||
user: User = self.get_object()
|
||||
token, __ = Token.objects.get_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
|
@ -1,7 +1,20 @@
|
||||
"""API Utilities"""
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Model
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField
|
||||
from rest_framework.serializers import (
|
||||
Serializer,
|
||||
SerializerMethodField,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
def is_dict(value: Any):
|
||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||
if isinstance(value, dict):
|
||||
return
|
||||
raise ValidationError("Value must be a dictionary.")
|
||||
|
||||
|
||||
class PassiveSerializer(Serializer):
|
||||
@ -35,6 +48,7 @@ class TypeCreateSerializer(PassiveSerializer):
|
||||
name = CharField(required=True)
|
||||
description = CharField(required=True)
|
||||
component = CharField(required=True)
|
||||
model_name = CharField(required=True)
|
||||
|
||||
|
||||
class CacheSerializer(PassiveSerializer):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Channels base classes"""
|
||||
from channels.exceptions import DenyConnection
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.auth import token_from_header
|
||||
@ -22,9 +23,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
|
||||
|
||||
raw_header = headers[b"authorization"]
|
||||
|
||||
token = token_from_header(raw_header)
|
||||
if not token:
|
||||
LOGGER.warning("Failed to authenticate")
|
||||
try:
|
||||
token = token_from_header(raw_header)
|
||||
# token is only None when no header was given, in which case we deny too
|
||||
if not token:
|
||||
raise DenyConnection()
|
||||
except AuthenticationFailed as exc:
|
||||
LOGGER.warning("Failed to authenticate", exc=exc)
|
||||
raise DenyConnection()
|
||||
|
||||
self.user = token.user
|
||||
|
@ -10,9 +10,13 @@
|
||||
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
|
||||
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
||||
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
@ -10,7 +10,7 @@
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<section class="ak-initial-load pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||
|
@ -10,7 +10,7 @@
|
||||
{% block body %}
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<section class="ak-initial-load pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||
|
15
authentik/core/tests/test_api_utils.py
Normal file
15
authentik/core/tests/test_api_utils.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Test API Utils"""
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.utils import is_dict
|
||||
|
||||
|
||||
class TestAPIUtils(APITestCase):
|
||||
"""Test API Utils"""
|
||||
|
||||
def test_is_dict(self):
|
||||
"""Test is_dict"""
|
||||
self.assertIsNone(is_dict({}))
|
||||
with self.assertRaises(ValidationError):
|
||||
is_dict("foo")
|
@ -2,7 +2,7 @@
|
||||
from django.urls.base import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, User
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
|
||||
|
||||
class TestTokenAPI(APITestCase):
|
||||
@ -19,4 +19,6 @@ class TestTokenAPI(APITestCase):
|
||||
reverse("authentik_api:token-list"), {"identifier": "test-token"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(Token.objects.get(identifier="test-token").user, self.user)
|
||||
token = Token.objects.get(identifier="test-token")
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
|
@ -153,10 +153,6 @@ class EventViewSet(ReadOnlyModelViewSet):
|
||||
data = []
|
||||
for value, name in EventAction.choices:
|
||||
data.append(
|
||||
{
|
||||
"name": name,
|
||||
"description": "",
|
||||
"component": value,
|
||||
}
|
||||
{"name": name, "description": "", "component": value, "model_name": ""}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -4,6 +4,7 @@ from typing import Iterable
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
@ -19,6 +20,12 @@ from authentik.lib.utils.reflection import all_subclasses
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class StageUserSettingSerializer(UserSettingSerializer):
|
||||
"""User settings but can include a configure flow"""
|
||||
|
||||
configure_flow = BooleanField(required=False)
|
||||
|
||||
|
||||
class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Stage Serializer"""
|
||||
|
||||
@ -73,12 +80,13 @@ class StageViewSet(
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
data = sorted(data, key=lambda x: x["name"])
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@swagger_auto_schema(responses={200: UserSettingSerializer(many=True)})
|
||||
@swagger_auto_schema(responses={200: StageUserSettingSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def user_settings(self, request: Request) -> Response:
|
||||
"""Get all stages the user can configure"""
|
||||
@ -89,6 +97,10 @@ class StageViewSet(
|
||||
if not user_settings:
|
||||
continue
|
||||
user_settings.initial_data["object_uid"] = str(stage.pk)
|
||||
if hasattr(stage, "configure_flow"):
|
||||
user_settings.initial_data["configure_flow"] = bool(
|
||||
stage.configure_flow
|
||||
)
|
||||
if not user_settings.is_valid():
|
||||
LOGGER.warning(user_settings.errors)
|
||||
matching_stages.append(user_settings.initial_data)
|
||||
|
@ -127,6 +127,7 @@ class FlowExecutorView(APIView):
|
||||
@swagger_auto_schema(
|
||||
responses={
|
||||
200: Challenge(),
|
||||
404: "No Token found", # This error can be raised by the email stage
|
||||
},
|
||||
request_body=no_body,
|
||||
manual_parameters=[
|
||||
|
@ -82,6 +82,7 @@ class ServiceConnectionViewSet(
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -8,14 +8,14 @@ from rest_framework.serializers import JSONField, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer, is_dict
|
||||
from authentik.outposts.models import Outpost, default_outpost_config
|
||||
|
||||
|
||||
class OutpostSerializer(ModelSerializer):
|
||||
"""Outpost Serializer"""
|
||||
|
||||
_config = JSONField()
|
||||
_config = JSONField(validators=[is_dict])
|
||||
providers_obj = ProviderSerializer(source="providers", many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -356,7 +356,7 @@ class Outpost(models.Model):
|
||||
intent=TokenIntents.INTENT_API,
|
||||
description=f"Autogenerated by authentik for Outpost {self.name}",
|
||||
expiring=False,
|
||||
managed="goauthentik.io/outpost",
|
||||
managed=f"goauthentik.io/outpost/{self.token_identifier}",
|
||||
)
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model]:
|
||||
|
@ -67,6 +67,8 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
|
||||
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
||||
self.set_uid(slugify(outpost.name))
|
||||
try:
|
||||
if not outpost.service_connection:
|
||||
return
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
service_connection = outpost.service_connection
|
||||
if isinstance(service_connection, DockerServiceConnection):
|
||||
|
@ -2,7 +2,7 @@
|
||||
from rest_framework.fields import BooleanField, CharField, JSONField, ListField
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer, is_dict
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ class PolicyTestSerializer(PassiveSerializer):
|
||||
"""Test policy execution for a user with context"""
|
||||
|
||||
user = PrimaryKeyRelatedField(queryset=User.objects.all())
|
||||
context = JSONField(required=False)
|
||||
context = JSONField(required=False, validators=[is_dict])
|
||||
|
||||
|
||||
class PolicyTestResultSerializer(PassiveSerializer):
|
||||
|
@ -108,6 +108,7 @@ class PolicyViewSet(
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -1,13 +1,23 @@
|
||||
"""Test authorize view"""
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.errors import (
|
||||
AuthorizeError,
|
||||
ClientIdError,
|
||||
RedirectUriError,
|
||||
)
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.providers.oauth2.models import (
|
||||
AuthorizationCode,
|
||||
GrantTypes,
|
||||
OAuth2Provider,
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
|
||||
|
||||
|
||||
@ -32,15 +42,194 @@ class TestViewsAuthorize(TestCase):
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_missing_redirect_uri(self):
|
||||
"""test missing redirect URI"""
|
||||
def test_request(self):
|
||||
"""test request param"""
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"request": "foo",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get(
|
||||
"/", data={"response_type": "code", "client_id": "test"}
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_response_type(self):
|
||||
"""test response_type"""
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type,
|
||||
GrantTypes.AUTHORIZATION_CODE,
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"scope": "openid",
|
||||
"state": "foo",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type,
|
||||
GrantTypes.IMPLICIT,
|
||||
)
|
||||
# Implicit without openid scope
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"state": "foo",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type,
|
||||
GrantTypes.IMPLICIT,
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code token",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"scope": "openid",
|
||||
"state": "foo",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
OAuthAuthorizationParams.from_request(request).grant_type, GrantTypes.HYBRID
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "invalid",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_full_code(self):
|
||||
"""Test full authorization"""
|
||||
flow = Flow.objects.create(slug="empty")
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_client_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"redirect_uri": "http://localhost",
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": f"http://localhost?code={code.code}&state={state}",
|
||||
},
|
||||
)
|
||||
|
||||
def test_full_implicit(self):
|
||||
"""Test full authorization"""
|
||||
flow = Flow.objects.create(slug="empty")
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_client_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
reverse("authentik_providers_oauth2:authorize"),
|
||||
data={
|
||||
"response_type": "id_token",
|
||||
"client_id": "test",
|
||||
"state": state,
|
||||
"scope": "openid",
|
||||
"redirect_uri": "http://localhost",
|
||||
},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
token: RefreshToken = RefreshToken.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
"to": (
|
||||
f"http://localhost#access_token={token.access_token}"
|
||||
f"&id_token={provider.encode(token.id_token.to_dict())}&token_type=bearer"
|
||||
f"&expires_in=600&state={state}"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
171
authentik/providers/oauth2/tests/test_views_token.py
Normal file
171
authentik/providers/oauth2/tests/test_views_token.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""Test token view"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_REFRESH_TOKEN,
|
||||
)
|
||||
from authentik.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from authentik.providers.oauth2.models import (
|
||||
AuthorizationCode,
|
||||
OAuth2Provider,
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.oauth2.views.token import TokenParams
|
||||
|
||||
|
||||
class TestViewsToken(TestCase):
|
||||
"""Test token view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_request_auth_code(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
code = AuthorizationCode.objects.create(
|
||||
code="foobar", provider=provider, user=user
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
params = TokenParams.from_request(request)
|
||||
self.assertEqual(params.provider, provider)
|
||||
|
||||
def test_request_refresh_token(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": token.refresh_token,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
params = TokenParams.from_request(request)
|
||||
self.assertEqual(params.provider, provider)
|
||||
|
||||
def test_auth_code_view(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
code = AuthorizationCode.objects.create(
|
||||
code="foobar", provider=provider, user=user
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
new_token: RefreshToken = RefreshToken.objects.filter(user=user).first()
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"access_token": new_token.access_token,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": 600,
|
||||
"id_token": provider.encode(
|
||||
new_token.id_token.to_dict(),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def test_refresh_token_view(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": token.refresh_token,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
new_token: RefreshToken = (
|
||||
RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first()
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"access_token": new_token.access_token,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": 600,
|
||||
"id_token": provider.encode(
|
||||
new_token.id_token.to_dict(),
|
||||
),
|
||||
},
|
||||
)
|
@ -3,6 +3,7 @@ import re
|
||||
from base64 import b64decode
|
||||
from binascii import Error
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.utils.cache import patch_vary_headers
|
||||
@ -25,15 +26,34 @@ class TokenResponse(JsonResponse):
|
||||
self["Pragma"] = "no-cache"
|
||||
|
||||
|
||||
def cors_allow_any(request, response):
|
||||
"""
|
||||
Add headers to permit CORS requests from any origin, with or without credentials,
|
||||
with any headers.
|
||||
"""
|
||||
def cors_allow_any(request: HttpRequest, response: HttpResponse, *allowed_origins: str):
|
||||
"""Add headers to permit CORS requests from any origin, with or without credentials,
|
||||
with any headers."""
|
||||
origin = request.META.get("HTTP_ORIGIN")
|
||||
if not origin:
|
||||
return response
|
||||
|
||||
# OPTIONS requests don't have an authorization header -> hence
|
||||
# we can't extract the provider this request is for
|
||||
# so for options requests we allow the calling origin without checking
|
||||
allowed = request.method == "OPTIONS"
|
||||
received_origin = urlparse(origin)
|
||||
for allowed_origin in allowed_origins:
|
||||
url = urlparse(allowed_origin)
|
||||
if (
|
||||
received_origin.scheme == url.scheme
|
||||
and received_origin.hostname == url.hostname
|
||||
and received_origin.port == url.port
|
||||
):
|
||||
allowed = True
|
||||
if not allowed:
|
||||
LOGGER.warning(
|
||||
"CORS: Origin is not an allowed origin",
|
||||
requested=origin,
|
||||
allowed=allowed_origins,
|
||||
)
|
||||
return response
|
||||
|
||||
# From the CORS spec: The string "*" cannot be used for a resource that supports credentials.
|
||||
response["Access-Control-Allow-Origin"] = origin
|
||||
patch_vary_headers(response, ["Origin"])
|
||||
|
@ -6,7 +6,7 @@ from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.response import Http404, HttpResponseRedirect
|
||||
from django.http.response import Http404, HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
@ -236,6 +236,9 @@ class OAuthFulfillmentStage(StageView):
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""final Stage of an OAuth2 Flow"""
|
||||
if PLAN_CONTEXT_PARAMS not in self.executor.plan.context:
|
||||
LOGGER.warning("Got to fulfillment stage with no pending context")
|
||||
return HttpResponseBadRequest()
|
||||
self.params: OAuthAuthorizationParams = self.executor.plan.context.pop(
|
||||
PLAN_CONTEXT_PARAMS
|
||||
)
|
||||
|
@ -30,6 +30,8 @@ PLAN_CONTEXT_SCOPES = "scopes"
|
||||
class ProviderInfoView(View):
|
||||
"""OpenID-compliant Provider Info"""
|
||||
|
||||
provider: OAuth2Provider
|
||||
|
||||
def get_info(self, provider: OAuth2Provider) -> dict[str, Any]:
|
||||
"""Get dictionary for OpenID Connect information"""
|
||||
scopes = list(
|
||||
@ -95,19 +97,20 @@ class ProviderInfoView(View):
|
||||
}
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(
|
||||
self, request: HttpRequest, application_slug: str, *args, **kwargs
|
||||
) -> HttpResponse:
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""OpenID-compliant Provider Info"""
|
||||
return JsonResponse(
|
||||
self.get_info(self.provider), json_dumps_params={"indent": 2}
|
||||
)
|
||||
|
||||
def dispatch(
|
||||
self, request: HttpRequest, application_slug: str, *args: Any, **kwargs: Any
|
||||
) -> HttpResponse:
|
||||
# Since this view only supports get, we can statically set the CORS headers
|
||||
application = get_object_or_404(Application, slug=application_slug)
|
||||
provider: OAuth2Provider = get_object_or_404(
|
||||
self.provider: OAuth2Provider = get_object_or_404(
|
||||
OAuth2Provider, pk=application.provider_id
|
||||
)
|
||||
return JsonResponse(self.get_info(provider), json_dumps_params={"indent": 2})
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# Since this view only supports get, we can statically set the CORS headers
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
cors_allow_any(request, response)
|
||||
cors_allow_any(request, response, *self.provider.redirect_uris.split("\n"))
|
||||
return response
|
||||
|
@ -198,7 +198,7 @@ class TokenView(View):
|
||||
response_dict = {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "Bearer",
|
||||
"token_type": "bearer",
|
||||
"expires_in": timedelta_from_string(
|
||||
self.params.provider.token_validity
|
||||
).seconds,
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""authentik OAuth2 OpenID Userinfo views"""
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
@ -22,6 +23,8 @@ class UserInfoView(View):
|
||||
"""Create a dictionary with all the requested claims about the End-User.
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse"""
|
||||
|
||||
token: Optional[RefreshToken]
|
||||
|
||||
def get_scope_descriptions(self, scopes: list[str]) -> list[dict[str, str]]:
|
||||
"""Get a list of all Scopes's descriptions"""
|
||||
scope_descriptions = []
|
||||
@ -79,16 +82,25 @@ class UserInfoView(View):
|
||||
final_claims.update(value)
|
||||
return final_claims
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
self.token = kwargs.get("token", None)
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
allowed_origins = []
|
||||
if self.token:
|
||||
allowed_origins = self.token.provider.redirect_uris.split("\n")
|
||||
cors_allow_any(self.request, response, *allowed_origins)
|
||||
return response
|
||||
|
||||
def options(self, request: HttpRequest) -> HttpResponse:
|
||||
return cors_allow_any(self.request, TokenResponse({}))
|
||||
return TokenResponse({})
|
||||
|
||||
def get(self, request: HttpRequest, **kwargs) -> HttpResponse:
|
||||
"""Handle GET Requests for UserInfo"""
|
||||
token: RefreshToken = kwargs["token"]
|
||||
claims = self.get_claims(token)
|
||||
claims["sub"] = token.id_token.sub
|
||||
if not self.token:
|
||||
return HttpResponseBadRequest()
|
||||
claims = self.get_claims(self.token)
|
||||
claims["sub"] = self.token.id_token.sub
|
||||
response = TokenResponse(claims)
|
||||
cors_allow_any(self.request, response)
|
||||
return response
|
||||
|
||||
def post(self, request: HttpRequest, **kwargs) -> HttpResponse:
|
||||
|
@ -21,7 +21,7 @@ class ProxyScopeMappingManager(ObjectManager):
|
||||
EnsureExists(
|
||||
ScopeMapping,
|
||||
"goauthentik.io/providers/proxy/scope-proxy",
|
||||
name="authentik default OAuth Mapping: proxy outpost",
|
||||
name="authentik default OAuth Mapping: Proxy outpost",
|
||||
scope_name=SCOPE_AK_PROXY,
|
||||
expression=SCOPE_AK_PROXY_EXPRESSION,
|
||||
),
|
||||
|
@ -1,13 +1,12 @@
|
||||
"""Channels Messages storage"""
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.messages.storage.base import Message
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.contrib.messages.storage.base import BaseStorage, Message
|
||||
from django.core.cache import cache
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
|
||||
class ChannelsStorage(FallbackStorage):
|
||||
class ChannelsStorage(BaseStorage):
|
||||
"""Send contrib.messages over websocket"""
|
||||
|
||||
def __init__(self, request: HttpRequest) -> None:
|
||||
@ -15,6 +14,9 @@ class ChannelsStorage(FallbackStorage):
|
||||
super().__init__(request)
|
||||
self.channel = get_channel_layer()
|
||||
|
||||
def _get(self):
|
||||
return [], True
|
||||
|
||||
def _store(self, messages: list[Message], response, *args, **kwargs):
|
||||
prefix = f"user_{self.request.session.session_key}_messages_"
|
||||
keys = cache.keys(f"{prefix}*")
|
||||
|
@ -143,7 +143,7 @@ SWAGGER_SETTINGS = {
|
||||
"authentik.api.pagination_schema.PaginationInspector",
|
||||
],
|
||||
"SECURITY_DEFINITIONS": {
|
||||
"token": {"type": "apiKey", "name": "Authorization", "in": "header"}
|
||||
"Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"}
|
||||
},
|
||||
}
|
||||
|
||||
@ -254,8 +254,8 @@ EMAIL_HOST = CONFIG.y("email.host")
|
||||
EMAIL_PORT = int(CONFIG.y("email.port"))
|
||||
EMAIL_HOST_USER = CONFIG.y("email.username")
|
||||
EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
|
||||
EMAIL_USE_TLS = CONFIG.y("email.use_tls")
|
||||
EMAIL_USE_SSL = CONFIG.y("email.use_ssl")
|
||||
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", True)
|
||||
EMAIL_USE_SSL = CONFIG.y_bool("email.use_ssl", False)
|
||||
EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
|
||||
DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
|
||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
||||
@ -338,6 +338,7 @@ if CONFIG.y("postgresql.s3_backup"):
|
||||
# Sentry integration
|
||||
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
|
||||
if not DEBUG and _ERROR_REPORTING:
|
||||
# pylint: disable=abstract-class-instantiated
|
||||
sentry_init(
|
||||
dsn="https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
integrations=[
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Sync LDAP Users and groups into authentik"""
|
||||
import ldap3
|
||||
import ldap3.core.exceptions
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
|
||||
|
||||
|
||||
@ -47,14 +49,17 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
"defaults": defaults,
|
||||
}
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
self._logger.warning("Failed to create group", exc=exc)
|
||||
self._logger.warning(
|
||||
(
|
||||
"To merge new group with existing group, set the group's "
|
||||
except (IntegrityError, FieldError) as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
f"Failed to create group: {str(exc)} "
|
||||
"To merge new group with existing group, set the groups's "
|
||||
f"Attribute '{LDAP_UNIQUENESS}' to '{uniq}'"
|
||||
)
|
||||
)
|
||||
),
|
||||
source=self._source,
|
||||
dn=group_dn,
|
||||
).save()
|
||||
else:
|
||||
self._logger.debug("Synced group", group=ak_group.name, created=created)
|
||||
group_count += 1
|
||||
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||
|
||||
import ldap3
|
||||
import ldap3.core.exceptions
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.utils import IntegrityError
|
||||
from pytz import UTC
|
||||
|
||||
@ -48,7 +49,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
"defaults": defaults,
|
||||
}
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
except (IntegrityError, FieldError) as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
|
@ -5,6 +5,7 @@ from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
@ -47,6 +48,20 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
"""Get source's type configuration"""
|
||||
return SourceTypeSerializer(instace.type).data
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
provider_type = MANAGER.find_type(attrs.get("provider_type", ""))
|
||||
for url in [
|
||||
"authorization_url",
|
||||
"access_token_url",
|
||||
"profile_url",
|
||||
]:
|
||||
if getattr(provider_type, url, None) is None:
|
||||
if url not in attrs:
|
||||
raise ValidationError(
|
||||
f"{url} is required for provider {provider_type.name}"
|
||||
)
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
model = OAuthSource
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
|
@ -9,6 +9,7 @@ from requests.models import Response
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -59,7 +60,16 @@ class BaseOAuthClient:
|
||||
args.update(additional)
|
||||
params = urlencode(args)
|
||||
LOGGER.info("redirect args", **args)
|
||||
return f"{self.source.authorization_url}?{params}"
|
||||
base_url = self.source.type.authorization_url
|
||||
if self.source.authorization_url:
|
||||
base_url = self.source.authorization_url
|
||||
if base_url == "":
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
source=self.source,
|
||||
message="Source has an empty authorization URL.",
|
||||
).save()
|
||||
return f"{base_url}?{params}"
|
||||
|
||||
def parse_raw_token(self, raw_token: str) -> dict[str, Any]:
|
||||
"Parse token and secret from raw token response."
|
||||
|
@ -28,9 +28,12 @@ class OAuthClient(BaseOAuthClient):
|
||||
if raw_token is not None and verifier is not None:
|
||||
token = self.parse_raw_token(raw_token)
|
||||
try:
|
||||
access_token_url: str = self.source.type.access_token_url or ""
|
||||
if self.source.access_token_url:
|
||||
access_token_url = self.source.access_token_url
|
||||
response = self.do_request(
|
||||
"post",
|
||||
self.source.access_token_url,
|
||||
access_token_url,
|
||||
token=token,
|
||||
headers=self._default_headers,
|
||||
oauth_verifier=verifier,
|
||||
@ -48,9 +51,12 @@ class OAuthClient(BaseOAuthClient):
|
||||
"Fetch the OAuth request token. Only required for OAuth 1.0."
|
||||
callback = self.request.build_absolute_uri(self.callback)
|
||||
try:
|
||||
request_token_url: str = self.source.type.request_token_url or ""
|
||||
if self.source.request_token_url:
|
||||
request_token_url = self.source.request_token_url
|
||||
response = self.do_request(
|
||||
"post",
|
||||
self.source.request_token_url,
|
||||
request_token_url,
|
||||
headers=self._default_headers,
|
||||
oauth_callback=callback,
|
||||
)
|
||||
|
@ -56,9 +56,12 @@ class OAuth2Client(BaseOAuthClient):
|
||||
LOGGER.warning("No code returned by the source")
|
||||
return None
|
||||
try:
|
||||
access_token_url = self.source.type.access_token_url or ""
|
||||
if self.source.access_token_url:
|
||||
access_token_url = self.source.access_token_url
|
||||
response = self.session.request(
|
||||
"post",
|
||||
self.source.access_token_url,
|
||||
access_token_url,
|
||||
data=args,
|
||||
headers=self._default_headers,
|
||||
)
|
||||
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 3.2 on 2021-04-16 07:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_oauth", "0002_auto_20200520_1108"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="access_token_url",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="URL used by authentik to retrive tokens.",
|
||||
max_length=255,
|
||||
verbose_name="Access Token URL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="authorization_url",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="URL the user is redirect to to conest the flow.",
|
||||
max_length=255,
|
||||
verbose_name="Authorization URL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="profile_url",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="URL used by authentik to get user information.",
|
||||
max_length=255,
|
||||
verbose_name="Profile URL",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,79 @@
|
||||
# Generated by Django 3.2 on 2021-04-17 19:00
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def update_empty_urls(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
OAuthSource = apps.get_model("authentik_sources_oauth", "oauthsource")
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for source in OAuthSource.objects.using(db_alias).all():
|
||||
changed = False
|
||||
if source.access_token_url == "":
|
||||
source.access_token_url = None
|
||||
changed = True
|
||||
if source.authorization_url == "":
|
||||
source.authorization_url = None
|
||||
changed = True
|
||||
if source.profile_url == "":
|
||||
source.profile_url = None
|
||||
changed = True
|
||||
if source.request_token_url == "":
|
||||
source.request_token_url = None
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
source.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_oauth", "0003_auto_20210416_0726"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="access_token_url",
|
||||
field=models.CharField(
|
||||
help_text="URL used by authentik to retrive tokens.",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="Access Token URL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="authorization_url",
|
||||
field=models.CharField(
|
||||
help_text="URL the user is redirect to to conest the flow.",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="Authorization URL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="profile_url",
|
||||
field=models.CharField(
|
||||
help_text="URL used by authentik to get user information.",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="Profile URL",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauthsource",
|
||||
name="request_token_url",
|
||||
field=models.CharField(
|
||||
help_text="URL used to request the initial token. This URL is only required for OAuth 1.",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="Request Token URL",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(update_empty_urls),
|
||||
]
|
@ -19,7 +19,7 @@ class OAuthSource(Source):
|
||||
|
||||
provider_type = models.CharField(max_length=255)
|
||||
request_token_url = models.CharField(
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=255,
|
||||
verbose_name=_("Request Token URL"),
|
||||
help_text=_(
|
||||
@ -28,16 +28,19 @@ class OAuthSource(Source):
|
||||
)
|
||||
authorization_url = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name=_("Authorization URL"),
|
||||
help_text=_("URL the user is redirect to to conest the flow."),
|
||||
)
|
||||
access_token_url = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name=_("Access Token URL"),
|
||||
help_text=_("URL used by authentik to retrive tokens."),
|
||||
)
|
||||
profile_url = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name=_("Profile URL"),
|
||||
help_text=_("URL used by authentik to get user information."),
|
||||
)
|
||||
@ -49,7 +52,7 @@ class OAuthSource(Source):
|
||||
"""Return the provider instance for this source"""
|
||||
from authentik.sources.oauth.types.manager import MANAGER
|
||||
|
||||
return MANAGER.find_type(self)
|
||||
return MANAGER.find_type(self.provider_type)
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
@ -160,6 +163,16 @@ class OpenIDOAuthSource(OAuthSource):
|
||||
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):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
|
@ -9,4 +9,5 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.twitter",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.oidc",
|
||||
"authentik.sources.oauth.types.plex",
|
||||
]
|
||||
|
@ -2,6 +2,7 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.sources.oauth.api.source import OAuthSourceSerializer
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
|
||||
@ -18,6 +19,31 @@ class TestOAuthSource(TestCase):
|
||||
consumer_key="",
|
||||
)
|
||||
|
||||
def test_api_validate(self):
|
||||
"""Test API validation"""
|
||||
self.assertTrue(
|
||||
OAuthSourceSerializer(
|
||||
data={
|
||||
"name": "foo",
|
||||
"slug": "bar",
|
||||
"provider_type": "google",
|
||||
"consumer_key": "foo",
|
||||
"consumer_secret": "foo",
|
||||
}
|
||||
).is_valid()
|
||||
)
|
||||
self.assertFalse(
|
||||
OAuthSourceSerializer(
|
||||
data={
|
||||
"name": "foo",
|
||||
"slug": "bar",
|
||||
"provider_type": "openid-connect",
|
||||
"consumer_key": "foo",
|
||||
"consumer_secret": "foo",
|
||||
}
|
||||
).is_valid()
|
||||
)
|
||||
|
||||
def test_source_redirect(self):
|
||||
"""test redirect view"""
|
||||
self.client.get(
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Source type manager"""
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -9,9 +9,6 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
|
||||
class RequestKind(Enum):
|
||||
"""Enum of OAuth Request types"""
|
||||
@ -58,24 +55,24 @@ class SourceTypeManager:
|
||||
"""Get list of tuples of all registered names"""
|
||||
return [(x.slug, x.name) for x in self.__sources]
|
||||
|
||||
def find_type(self, source: "OAuthSource") -> SourceType:
|
||||
def find_type(self, type_name: str) -> SourceType:
|
||||
"""Find type based on source"""
|
||||
found_type = None
|
||||
for src_type in self.__sources:
|
||||
if src_type.slug == source.provider_type:
|
||||
if src_type.slug == type_name:
|
||||
return src_type
|
||||
if not found_type:
|
||||
found_type = SourceType()
|
||||
LOGGER.warning(
|
||||
"no matching type found, using default",
|
||||
wanted=source.provider_type,
|
||||
have=[x.name for x in self.__sources],
|
||||
wanted=type_name,
|
||||
have=[x.slug for x in self.__sources],
|
||||
)
|
||||
return found_type
|
||||
|
||||
def find(self, source: "OAuthSource", kind: RequestKind) -> Callable:
|
||||
def find(self, type_name: str, kind: RequestKind) -> Callable:
|
||||
"""Find fitting Source Type"""
|
||||
found_type = self.find_type(source)
|
||||
found_type = self.find_type(type_name)
|
||||
if kind == RequestKind.CALLBACK:
|
||||
return found_type.callback_view
|
||||
if kind == RequestKind.REDIRECT:
|
||||
|
134
authentik/sources/oauth/types/plex.py
Normal file
134
authentik/sources/oauth/types/plex.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""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 = ""
|
@ -209,9 +209,9 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_sources_oauth:oauth-client-user",
|
||||
kwargs={"source_slug": self.source.slug},
|
||||
"authentik_core:if-admin",
|
||||
)
|
||||
+ f"#/user;page-{self.source.slug}"
|
||||
)
|
||||
|
||||
def handle_enroll(
|
||||
|
@ -21,6 +21,6 @@ class DispatcherView(View):
|
||||
if not slug:
|
||||
raise Http404
|
||||
source = get_object_or_404(OAuthSource, slug=slug)
|
||||
view = MANAGER.find(source, kind=RequestKind(self.kind))
|
||||
view = MANAGER.find(source.provider_type, kind=RequestKind(self.kind))
|
||||
LOGGER.debug("dispatching OAuth2 request to", view=view, kind=self.kind)
|
||||
return view.as_view()(*args, **kwargs)
|
||||
|
@ -52,7 +52,6 @@ class EmailStageViewSet(ModelViewSet):
|
||||
queryset = EmailStage.objects.all()
|
||||
serializer_class = EmailStageSerializer
|
||||
|
||||
# TODO: Validate connection settings when use_global_settings is unchecked
|
||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def templates(self, request: Request) -> Response:
|
||||
@ -64,6 +63,7 @@ class EmailStageViewSet(ModelViewSet):
|
||||
"name": value,
|
||||
"description": label,
|
||||
"component": "",
|
||||
"model_name": "",
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(choices, many=True).data)
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""Invitation Stage API Views"""
|
||||
from rest_framework.fields import JSONField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import is_dict
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
|
||||
@ -27,6 +29,8 @@ class InvitationStageViewSet(ModelViewSet):
|
||||
class InvitationSerializer(ModelSerializer):
|
||||
"""Invitation Serializer"""
|
||||
|
||||
fixed_data = JSONField(validators=[is_dict], required=False)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Invitation
|
||||
|
@ -142,7 +142,9 @@ class TestInvitationsAPI(APITestCase):
|
||||
def test_invite_create(self):
|
||||
"""Test Invitations creation endpoint"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:invitation-list"), {"identifier": "test-token"}
|
||||
reverse("authentik_api:invitation-list"),
|
||||
{"identifier": "test-token", "fixed_data": {}},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(Invitation.objects.first().created_by, self.user)
|
||||
|
@ -4,6 +4,7 @@ version: '3.2'
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:12
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
networks:
|
||||
@ -19,7 +20,8 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.4.1-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.4.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
@ -33,8 +35,6 @@ services:
|
||||
- ./media:/media
|
||||
- ./custom-templates:/templates
|
||||
- geoip:/geoip
|
||||
ports:
|
||||
- 8000
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
@ -48,7 +48,8 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.4.1-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.4.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -67,7 +68,8 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: ${AUTHENTIK_IMAGE_STATIC:-beryju/authentik-static}:${AUTHENTIK_TAG:-2021.4.1-rc1}
|
||||
image: ${AUTHENTIK_IMAGE_STATIC:-beryju/authentik-static}:${AUTHENTIK_TAG:-2021.4.3}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
@ -76,13 +78,14 @@ services:
|
||||
traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/if`, `/media`, `/robots.txt`, `/favicon.ico`)
|
||||
traefik.http.routers.static-router.tls: 'true'
|
||||
traefik.http.routers.static-router.service: static-service
|
||||
traefik.http.services.static-service.loadbalancer.healthcheck.path: /-/health/ready/
|
||||
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
|
||||
traefik.http.services.static-service.loadbalancer.healthcheck.interval: 30s
|
||||
traefik.http.services.static-service.loadbalancer.server.port: '80'
|
||||
volumes:
|
||||
- ./media:/usr/share/nginx/html/media
|
||||
traefik:
|
||||
image: traefik:2.3
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--log.format=json"
|
||||
- "--api.insecure=true"
|
||||
|
@ -4,7 +4,7 @@ name: authentik
|
||||
home: https://goauthentik.io
|
||||
sources:
|
||||
- https://github.com/BeryJu/authentik
|
||||
version: "2021.4.1-rc1"
|
||||
version: "2021.4.3"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/web/icons/icon.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
|
@ -4,12 +4,12 @@
|
||||
|-----------------------------------|-------------------------|-------------|
|
||||
| image.name | beryju/authentik | Image used to run the authentik server and worker |
|
||||
| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
|
||||
| image.tag | 2021.4.1-rc1 | Image tag |
|
||||
| image.tag | 2021.4.3 | Image tag |
|
||||
| image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments |
|
||||
| serverReplicas | 1 | Replicas for the Server deployment |
|
||||
| workerReplicas | 1 | Replicas for the Worker deployment |
|
||||
| kubernetesIntegration | true | Enable/disable the Kubernetes integration for authentik. This will create a service account for authentik to create and update outposts in authentik |
|
||||
| config.secretKey | | Secret key used to sign session cookies, generate with `pwgen 50 1` for example. |
|
||||
| config.secretKey | | Secret key used to sign session cookies, generate with `pwgen 50 1` or `openssl rand -base64 36` for example. |
|
||||
| config.errorReporting.enabled | false | Enable/disable error reporting |
|
||||
| config.errorReporting.environment | customer | Environment sent with the error reporting |
|
||||
| config.errorReporting.sendPii | false | Whether to send Personally-identifiable data with the error reporting |
|
||||
@ -22,6 +22,11 @@
|
||||
| config.email.use_ssl | false | Enable SSL |
|
||||
| config.email.timeout | 10 | SMTP Timeout |
|
||||
| config.email.from | authentik@localhost | Email address authentik will send from, should have a correct @domain |
|
||||
| pvc.mode | ReadWriteMany | Mode that the PVCs are created in (uploads and GeoIP, if enabled) |
|
||||
| pvc.uploadsSize | 5Gi | Size for the uploads PVC |
|
||||
| pvc.uploadsStorageClass | null | Storage class for the uploads PVC (default: use default storage class) |
|
||||
| pvc.geoIpSize | 1Gi | Size for the GeoIP PVC |
|
||||
| pvc.geoIpStorageClass | null | Storage class for the GeoIP PVC (default: use default storage class) |
|
||||
| geoip.enabled | false | Optionally enable GeoIP |
|
||||
| geoip.accountId | | GeoIP MaxMind Account ID |
|
||||
| geoip.licenseKey | | GeoIP MaxMind License key |
|
||||
|
@ -10,8 +10,9 @@ metadata:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
- {{ .Values.pvc.mode }}
|
||||
storageClassName: {{ .Values.pvc.geoIpStorageClass }}
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storage: {{ .Values.pvc.geoIpSize }}
|
||||
{{- end }}
|
||||
|
@ -9,7 +9,8 @@ metadata:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
- {{ .Values.pvc.mode }}
|
||||
storageClassName: {{ .Values.pvc.uploadsStorageClass }}
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
storage: {{ .Values.pvc.uploadsSize }}
|
||||
|
@ -43,29 +43,6 @@ spec:
|
||||
values:
|
||||
- web
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
initContainers:
|
||||
- name: authentik-database-migrations
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
args: [migrate]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "authentik.fullname" . }}-config
|
||||
prefix: AUTHENTIK_
|
||||
- secretRef:
|
||||
name: {{ include "authentik.fullname" . }}-secret-key
|
||||
prefix: AUTHENTIK_
|
||||
env:
|
||||
- name: AUTHENTIK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: postgresql-password
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
|
@ -5,7 +5,7 @@ image:
|
||||
name: beryju/authentik
|
||||
name_static: beryju/authentik-static
|
||||
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
|
||||
tag: 2021.4.1-rc1
|
||||
tag: 2021.4.3
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
serverReplicas: 1
|
||||
@ -15,7 +15,14 @@ workerReplicas: 1
|
||||
kubernetesIntegration: true
|
||||
|
||||
monitoring:
|
||||
enabled: true
|
||||
enabled: false
|
||||
|
||||
pvc:
|
||||
mode: ReadWriteMany
|
||||
uploadsSize: 5Gi
|
||||
uploadsStorageClass: null
|
||||
geoIpSize: 1Gi
|
||||
geoIpStorageClass: null
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
|
@ -2,13 +2,13 @@
|
||||
python -m lifecycle.wait_for_db
|
||||
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" > /dev/stderr
|
||||
if [[ "$1" == "server" ]]; then
|
||||
python -m lifecycle.migrate
|
||||
gunicorn -c /lifecycle/gunicorn.conf.py authentik.root.asgi:application
|
||||
elif [[ "$1" == "worker" ]]; then
|
||||
celery -A authentik.root.celery worker --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events
|
||||
elif [[ "$1" == "migrate" ]]; then
|
||||
# Run system migrations first, run normal migrations after
|
||||
printf "DEPERECATED: database migrations are now executed automatically on startup."
|
||||
python -m lifecycle.migrate
|
||||
python -m manage migrate
|
||||
elif [[ "$1" == "backup" ]]; then
|
||||
python -m manage dbbackup --clean
|
||||
elif [[ "$1" == "restore" ]]; then
|
||||
|
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
"""System Migration handler"""
|
||||
import os
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from inspect import getmembers, isclass
|
||||
from pathlib import Path
|
||||
@ -11,6 +12,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
ADV_LOCK_UID = 1000
|
||||
|
||||
|
||||
class BaseMigration:
|
||||
@ -40,18 +42,36 @@ if __name__ == "__main__":
|
||||
host=CONFIG.y("postgresql.host"),
|
||||
)
|
||||
curr = conn.cursor()
|
||||
# lock an advisory lock to prevent multiple instances from migrating at once
|
||||
LOGGER.info("waiting to acquire database lock")
|
||||
curr.execute("SELECT pg_advisory_lock(%s)", (ADV_LOCK_UID,))
|
||||
try:
|
||||
for migration in (
|
||||
Path(__file__).parent.absolute().glob("system_migrations/*.py")
|
||||
):
|
||||
spec = spec_from_file_location("lifecycle.system_migrations", migration)
|
||||
mod = module_from_spec(spec)
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
for migration in Path(__file__).parent.absolute().glob("system_migrations/*.py"):
|
||||
spec = spec_from_file_location("lifecycle.system_migrations", migration)
|
||||
mod = module_from_spec(spec)
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
for name, sub in getmembers(mod, isclass):
|
||||
if name != "Migration":
|
||||
continue
|
||||
migration = sub(curr, conn)
|
||||
if migration.needs_migration():
|
||||
LOGGER.info("Migration needs to be applied", migration=sub)
|
||||
migration.run()
|
||||
LOGGER.info("Migration finished applying", migration=sub)
|
||||
for name, sub in getmembers(mod, isclass):
|
||||
if name != "Migration":
|
||||
continue
|
||||
migration = sub(curr, conn)
|
||||
if migration.needs_migration():
|
||||
LOGGER.info("Migration needs to be applied", migration=sub)
|
||||
migration.run()
|
||||
LOGGER.info("Migration finished applying", migration=sub)
|
||||
LOGGER.info("applying django migrations")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(["", "migrate"])
|
||||
finally:
|
||||
curr.execute("SELECT pg_advisory_unlock(%s)", (ADV_LOCK_UID,))
|
||||
|
@ -18,15 +18,12 @@ stages:
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
inputs:
|
||||
version: '1.15'
|
||||
version: '1.16.3'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install gnupg ca-certificates
|
||||
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61
|
||||
echo "deb https://dl.bintray.com/go-swagger/goswagger-debian ubuntu main" | sudo tee /etc/apt/sources.list.d/goswagger.list
|
||||
sudo apt update
|
||||
sudo apt install swagger
|
||||
sudo wget -O /usr/local/bin/swagger https://github.com/go-swagger/go-swagger/releases/latest/download/swagger_linux_amd64
|
||||
sudo chmod +x /usr/local/bin/swagger
|
||||
mkdir -p $(go env GOPATH)
|
||||
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
|
||||
workingDirectory: 'outpost/'
|
||||
@ -43,11 +40,7 @@ stages:
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
inputs:
|
||||
version: '1.15'
|
||||
- task: Go@0
|
||||
inputs:
|
||||
command: 'get'
|
||||
arguments: '-u golang.org/x/lint/golint'
|
||||
version: '1.16.3'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
@ -56,7 +49,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
$(go list -f {{.Target}} golang.org/x/lint/golint) ./...
|
||||
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.39.0 golangci-lint run -v --timeout 200s
|
||||
workingDirectory: 'outpost/'
|
||||
- stage: proxy_build_go
|
||||
jobs:
|
||||
@ -66,7 +59,7 @@ stages:
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
inputs:
|
||||
version: '1.15'
|
||||
version: '1.16.3'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
@ -85,7 +78,7 @@ stages:
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
inputs:
|
||||
version: '1.15'
|
||||
version: '1.16.3'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
|
@ -52,13 +52,14 @@ func main() {
|
||||
|
||||
ac.Server = proxy.NewServer(ac)
|
||||
|
||||
ac.Start()
|
||||
err = ac.Start()
|
||||
if err != nil {
|
||||
log.WithError(err).Panic("Failed to run server")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-interrupt:
|
||||
ac.Shutdown()
|
||||
os.Exit(0)
|
||||
}
|
||||
<-interrupt
|
||||
ac.Shutdown()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/getsentry/sentry-go v0.10.0
|
||||
github.com/go-openapi/analysis v0.20.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.0
|
||||
github.com/go-openapi/runtime v0.19.27
|
||||
github.com/go-openapi/runtime v0.19.28
|
||||
github.com/go-openapi/strfmt v0.20.1
|
||||
github.com/go-openapi/swag v0.19.15
|
||||
github.com/go-openapi/validate v0.20.2
|
||||
github.com/go-redis/redis/v7 v7.4.0 // indirect
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/justinas/alice v1.2.0
|
||||
@ -20,9 +21,9 @@ require (
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
|
||||
github.com/recws-org/recws v1.3.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
@ -30,9 +31,10 @@ require (
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 // indirect
|
||||
golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
|
@ -154,8 +154,9 @@ github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9sn
|
||||
github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
|
||||
github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ=
|
||||
github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk=
|
||||
github.com/go-openapi/analysis v0.20.0 h1:UN09o0kNhleunxW7LR+KnltD0YrJ8FF03pSqvAN3Vro=
|
||||
github.com/go-openapi/analysis v0.20.0/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og=
|
||||
github.com/go-openapi/analysis v0.20.1 h1:zdVbw8yoD4SWZeq+cWdGgquaB0W4VrsJvDJHJND/Ktc=
|
||||
github.com/go-openapi/analysis v0.20.1/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og=
|
||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
|
||||
@ -195,8 +196,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g
|
||||
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
|
||||
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
|
||||
github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
|
||||
github.com/go-openapi/runtime v0.19.27 h1:4zrQCJoP7rqNCUaApDv1MdPkaa5TuPfO05Lq5WLhX9I=
|
||||
github.com/go-openapi/runtime v0.19.27/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
|
||||
github.com/go-openapi/runtime v0.19.28 h1:9lYu6axek8LJrVkMVViVirRcpoaCxXX7+sSvmizGVnA=
|
||||
github.com/go-openapi/runtime v0.19.28/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M=
|
||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
||||
@ -300,8 +301,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
@ -316,8 +318,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@ -503,8 +506,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=
|
||||
github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
@ -518,8 +521,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e h1:BLqxdwZ6j771IpSCRx7s/GJjXHUE00Hmu7/YegCGdzA=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac h1:jWKYCNlX4J5s8M0nHYkh7Y7c9gRVDEb3mq51j5J0F5M=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
@ -662,8 +665,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -738,15 +741,16 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5 h1:zuP3axpB9rV3xH0EA7n3/gCrNPZm2SRl0l4mVH2BRj4=
|
||||
golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs=
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 h1:Mj83v+wSRNEar42a/MQgxk9X42TdEmrOl9i+y8WbxLo=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -806,9 +810,9 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -816,8 +820,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -957,8 +962,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
@ -39,12 +40,12 @@ type APIController struct {
|
||||
}
|
||||
|
||||
// NewAPIController initialise new API Controller instance from URL and API token
|
||||
func NewAPIController(pbURL url.URL, token string) *APIController {
|
||||
transport := httptransport.New(pbURL.Host, client.DefaultBasePath, []string{pbURL.Scheme})
|
||||
func NewAPIController(akURL url.URL, token string) *APIController {
|
||||
transport := httptransport.New(akURL.Host, client.DefaultBasePath, []string{akURL.Scheme})
|
||||
transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION))
|
||||
|
||||
// create the transport
|
||||
auth := httptransport.BasicAuth("", token)
|
||||
auth := httptransport.BearerToken(token)
|
||||
|
||||
// create the API client, with the transport
|
||||
apiClient := client.New(transport, strfmt.Default)
|
||||
@ -53,10 +54,11 @@ func NewAPIController(pbURL url.URL, token string) *APIController {
|
||||
|
||||
// Because we don't know the outpost UUID, we simply do a list and pick the first
|
||||
// The service account this token belongs to should only have access to a single outpost
|
||||
outposts, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth)
|
||||
outposts, err := apiClient.Outposts.OutpostsInstancesList(outposts.NewOutpostsInstancesListParams(), auth)
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Panic("Failed to fetch configuration")
|
||||
log.WithError(err).Error("Failed to fetch configuration")
|
||||
os.Exit(1)
|
||||
}
|
||||
outpost := outposts.Payload.Results[0]
|
||||
doGlobalSetup(outpost.Config.(map[string]interface{}))
|
||||
@ -73,7 +75,7 @@ func NewAPIController(pbURL url.URL, token string) *APIController {
|
||||
lastBundleHash: "",
|
||||
}
|
||||
ac.logger.Debugf("HA Reload offset: %s", ac.reloadOffset)
|
||||
ac.initWS(pbURL, outpost.Pk)
|
||||
ac.initWS(akURL, outpost.Pk)
|
||||
return ac
|
||||
}
|
||||
|
||||
@ -96,7 +98,10 @@ func (a *APIController) Start() error {
|
||||
a.startWSHealth()
|
||||
}()
|
||||
go func() {
|
||||
a.Server.Start()
|
||||
err := a.Server.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package ak
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -20,7 +19,7 @@ func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) {
|
||||
pathTemplate := "%s://%s/ws/outpost/%s/"
|
||||
scheme := strings.ReplaceAll(pbURL.Scheme, "http", "ws")
|
||||
|
||||
authHeader := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("Basic :%s", ac.token)))
|
||||
authHeader := fmt.Sprintf("Bearer %s", ac.token)
|
||||
|
||||
header := http.Header{
|
||||
"Authorization": []string{authHeader},
|
||||
@ -65,7 +64,6 @@ func (ac *APIController) Shutdown() {
|
||||
ac.logger.Println("write close:", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ac *APIController) startWSHandler() {
|
||||
@ -93,7 +91,8 @@ func (ac *APIController) startWSHandler() {
|
||||
}
|
||||
|
||||
func (ac *APIController) startWSHealth() {
|
||||
for ; true; <-time.Tick(time.Second * 10) {
|
||||
ticker := time.NewTicker(time.Second * 10)
|
||||
for ; true; <-ticker.C {
|
||||
if !ac.wsConn.IsConnected() {
|
||||
continue
|
||||
}
|
||||
|
@ -38,7 +38,11 @@ func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *opti
|
||||
return nil
|
||||
}
|
||||
providerOpts := &options.Options{}
|
||||
copier.Copy(&providerOpts, getCommonOptions())
|
||||
err = copier.Copy(&providerOpts, getCommonOptions())
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Failed to copy options, skipping provider")
|
||||
return nil
|
||||
}
|
||||
providerOpts.ClientID = provider.ClientID
|
||||
providerOpts.ClientSecret = provider.ClientSecret
|
||||
|
||||
@ -62,7 +66,7 @@ func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *opti
|
||||
ID: "default",
|
||||
URI: *provider.InternalHost,
|
||||
Path: "/",
|
||||
InsecureSkipTLSVerify: *&provider.InternalHostSslValidation,
|
||||
InsecureSkipTLSVerify: provider.InternalHostSslValidation,
|
||||
},
|
||||
}
|
||||
|
||||
@ -85,7 +89,7 @@ func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *opti
|
||||
return providerOpts
|
||||
}
|
||||
|
||||
x509cert, err := tls.X509KeyPair([]byte(*&cert.Payload.Data), []byte(key.Payload.Data))
|
||||
x509cert, err := tls.X509KeyPair([]byte(cert.Payload.Data), []byte(key.Payload.Data))
|
||||
if err != nil {
|
||||
pb.log.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to parse certificate")
|
||||
return providerOpts
|
||||
@ -135,7 +139,7 @@ func (pb *providerBundle) Build(provider *models.ProxyOutpostConfig) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *&provider.BasicAuthEnabled {
|
||||
if provider.BasicAuthEnabled {
|
||||
oauthproxy.SetBasicAuth = true
|
||||
oauthproxy.BasicAuthUserAttribute = provider.BasicAuthUserAttribute
|
||||
oauthproxy.BasicAuthPasswordAttribute = provider.BasicAuthPasswordAttribute
|
||||
|
@ -1,3 +1,3 @@
|
||||
package pkg
|
||||
|
||||
const VERSION = "2021.4.1-rc1"
|
||||
const VERSION = "2021.4.3"
|
||||
|
361
swagger.yaml
361
swagger.yaml
@ -13,12 +13,12 @@ consumes:
|
||||
produces:
|
||||
- application/json
|
||||
securityDefinitions:
|
||||
token:
|
||||
Bearer:
|
||||
type: apiKey
|
||||
name: Authorization
|
||||
in: header
|
||||
security:
|
||||
- token: []
|
||||
- Bearer: []
|
||||
paths:
|
||||
/admin/apps/:
|
||||
get:
|
||||
@ -131,27 +131,7 @@ paths:
|
||||
get:
|
||||
operationId: admin_version_list
|
||||
description: Get running and latest version.
|
||||
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
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@ -1172,6 +1152,9 @@ paths:
|
||||
description: Page Size
|
||||
required: false
|
||||
type: integer
|
||||
- name: superuser_full_list
|
||||
in: query
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@ -2257,15 +2240,15 @@ paths:
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/Link'
|
||||
'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'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- core
|
||||
parameters:
|
||||
@ -3684,15 +3667,15 @@ paths:
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/Challenge'
|
||||
'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'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- flows
|
||||
post:
|
||||
@ -4386,6 +4369,281 @@ paths:
|
||||
description: A unique integer value identifying this OAuth2 Token.
|
||||
required: true
|
||||
type: integer
|
||||
/outposts/instances/:
|
||||
get:
|
||||
operationId: outposts_instances_list
|
||||
description: Outpost Viewset
|
||||
parameters:
|
||||
- name: providers__isnull
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- 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/Outpost'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- outposts
|
||||
post:
|
||||
operationId: outposts_instances_create
|
||||
description: Outpost Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
responses:
|
||||
'201':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- outposts
|
||||
parameters: []
|
||||
/outposts/instances/default_settings/:
|
||||
get:
|
||||
operationId: outposts_instances_default_settings
|
||||
description: Global default outpost config
|
||||
parameters:
|
||||
- name: providers__isnull
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- 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:
|
||||
$ref: '#/definitions/OutpostDefaultConfig'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- outposts
|
||||
parameters: []
|
||||
/outposts/instances/{uuid}/:
|
||||
get:
|
||||
operationId: outposts_instances_read
|
||||
description: Outpost Viewset
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
'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:
|
||||
- outposts
|
||||
put:
|
||||
operationId: outposts_instances_update
|
||||
description: Outpost Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
'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:
|
||||
- outposts
|
||||
patch:
|
||||
operationId: outposts_instances_partial_update
|
||||
description: Outpost Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/Outpost'
|
||||
'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:
|
||||
- outposts
|
||||
delete:
|
||||
operationId: outposts_instances_delete
|
||||
description: Outpost 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:
|
||||
- outposts
|
||||
parameters:
|
||||
- name: uuid
|
||||
in: path
|
||||
description: A UUID string identifying this outpost.
|
||||
required: true
|
||||
type: string
|
||||
format: uuid
|
||||
/outposts/instances/{uuid}/health/:
|
||||
get:
|
||||
operationId: outposts_instances_health
|
||||
description: Get outposts current health
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/OutpostHealth'
|
||||
'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:
|
||||
- outposts
|
||||
parameters:
|
||||
- name: uuid
|
||||
in: path
|
||||
description: A UUID string identifying this outpost.
|
||||
required: true
|
||||
type: string
|
||||
format: uuid
|
||||
/outposts/outposts/:
|
||||
get:
|
||||
operationId: outposts_outposts_list
|
||||
@ -7691,6 +7949,9 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PolicyTest'
|
||||
- name: format_result
|
||||
in: query
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@ -10285,7 +10546,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/UserSetting'
|
||||
$ref: '#/definitions/StageUserSetting'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
@ -14791,6 +15052,7 @@ definitions:
|
||||
- name
|
||||
- description
|
||||
- component
|
||||
- model_name
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
@ -14805,6 +15067,10 @@ definitions:
|
||||
title: Component
|
||||
type: string
|
||||
minLength: 1
|
||||
model_name:
|
||||
title: Model name
|
||||
type: string
|
||||
minLength: 1
|
||||
EventTopPerUser:
|
||||
required:
|
||||
- application
|
||||
@ -16983,9 +17249,6 @@ definitions:
|
||||
- name
|
||||
- slug
|
||||
- provider_type
|
||||
- authorization_url
|
||||
- access_token_url
|
||||
- profile_url
|
||||
- consumer_key
|
||||
- consumer_secret
|
||||
type: object
|
||||
@ -17052,24 +17315,29 @@ definitions:
|
||||
for OAuth 1.
|
||||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
authorization_url:
|
||||
title: Authorization URL
|
||||
description: URL the user is redirect to to conest the flow.
|
||||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
access_token_url:
|
||||
title: Access Token URL
|
||||
description: URL used by authentik to retrive tokens.
|
||||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
profile_url:
|
||||
title: Profile URL
|
||||
description: URL used by authentik to get user information.
|
||||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
consumer_key:
|
||||
title: Consumer key
|
||||
type: string
|
||||
@ -17242,6 +17510,28 @@ definitions:
|
||||
\ log out manually. (Format: hours=1;minutes=2;seconds=3)."
|
||||
type: string
|
||||
minLength: 1
|
||||
StageUserSetting:
|
||||
required:
|
||||
- object_uid
|
||||
- component
|
||||
- title
|
||||
type: object
|
||||
properties:
|
||||
object_uid:
|
||||
title: Object uid
|
||||
type: string
|
||||
minLength: 1
|
||||
component:
|
||||
title: Component
|
||||
type: string
|
||||
minLength: 1
|
||||
title:
|
||||
title: Title
|
||||
type: string
|
||||
minLength: 1
|
||||
configure_flow:
|
||||
title: Configure flow
|
||||
type: boolean
|
||||
AuthenticatorStaticStage:
|
||||
required:
|
||||
- name
|
||||
@ -17725,7 +18015,6 @@ definitions:
|
||||
x-nullable: true
|
||||
fixed_data:
|
||||
title: Fixed data
|
||||
description: Optional fixed data to enforce on user enrollment.
|
||||
type: object
|
||||
created_by:
|
||||
required:
|
||||
|
1
web/authentik/sources/plex.svg
Normal file
1
web/authentik/sources/plex.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title/><path d="M11.643 0H4.68l7.679 12L4.68 24h6.963l7.677-12-7.677-12"/></svg>
|
After Width: | Height: | Size: 175 B |
@ -81,7 +81,7 @@ http {
|
||||
location /static/ {
|
||||
expires 31d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
add_header X-authentik-version "2021.4.1-rc1";
|
||||
add_header X-authentik-version "2021.4.3";
|
||||
add_header Vary X-authentik-version;
|
||||
}
|
||||
|
||||
|
277
web/package-lock.json
generated
277
web/package-lock.json
generated
@ -1336,28 +1336,86 @@
|
||||
}
|
||||
},
|
||||
"@lingui/babel-plugin-extract-messages": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.8.6.tgz",
|
||||
"integrity": "sha512-SJoeg6G+vh3AYWDBuwsXitV7YTp9JxUwjPll+TKIsuMITvaOq8+lm1gt9fIhu1FiwmoddmW9wNd9+R231I4TiQ==",
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.8.9.tgz",
|
||||
"integrity": "sha512-zPpSl89nvUrLyGHfVosZHCP9fylfCfkEMc29wGdjE6f0U+frJ59NRLilWMy7xaE8uz97cD5vkhYaaF1wnavhxA==",
|
||||
"requires": {
|
||||
"@babel/generator": "^7.11.6",
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@lingui/conf": "^3.8.6",
|
||||
"@lingui/conf": "^3.8.9",
|
||||
"mkdirp": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/conf": {
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-3.8.9.tgz",
|
||||
"integrity": "sha512-r0RGchwiALjCE6CSOtOKbOqVrNg1EQ78AXjyvbrtJoPWVlChDasWCckXEF0BSnsoZaRP6nQCAI+dsQiGW1deWg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"jest-validate": "^26.5.2",
|
||||
"lodash.get": "^4.4.2"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@lingui/cli": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/cli/-/cli-3.8.6.tgz",
|
||||
"integrity": "sha512-D3r6U6W/CGch8rEd/IFgImhoIlKePtyWLEwmybKnNfXjbtWBVtnVWroHl5IHYkXw9oGbOwxJDERMduR3D6IItw==",
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/cli/-/cli-3.8.9.tgz",
|
||||
"integrity": "sha512-UccLtfwrTjXrZcTxpqA4ggYhuUMbXZtzbUVks8nDVt3emVqU56C3VMvVD8WKXLL8Qmq9cEDXPwIZy7IKRL4mEQ==",
|
||||
"requires": {
|
||||
"@babel/generator": "^7.11.6",
|
||||
"@babel/parser": "^7.11.5",
|
||||
"@babel/plugin-syntax-jsx": "^7.10.4",
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@babel/types": "^7.11.5",
|
||||
"@lingui/babel-plugin-extract-messages": "^3.8.6",
|
||||
"@lingui/conf": "^3.8.6",
|
||||
"@lingui/babel-plugin-extract-messages": "^3.8.9",
|
||||
"@lingui/conf": "^3.8.9",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"bcp-47": "^1.0.7",
|
||||
"chalk": "^4.1.0",
|
||||
@ -1384,6 +1442,19 @@
|
||||
"ramda": "^0.27.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/conf": {
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-3.8.9.tgz",
|
||||
"integrity": "sha512-r0RGchwiALjCE6CSOtOKbOqVrNg1EQ78AXjyvbrtJoPWVlChDasWCckXEF0BSnsoZaRP6nQCAI+dsQiGW1deWg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"jest-validate": "^26.5.2",
|
||||
"lodash.get": "^4.4.2"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@ -1460,9 +1531,9 @@
|
||||
}
|
||||
},
|
||||
"@lingui/conf": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-3.8.6.tgz",
|
||||
"integrity": "sha512-qfTzii2IIt5CBE3vIKYHHY0cH8Nz6gRnIU6DnJhQD76LvdzplAwa7Pt/QwIGJKaV9IvfFZCrNAM34P/hSKl5uQ==",
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-3.8.9.tgz",
|
||||
"integrity": "sha512-r0RGchwiALjCE6CSOtOKbOqVrNg1EQ78AXjyvbrtJoPWVlChDasWCckXEF0BSnsoZaRP6nQCAI+dsQiGW1deWg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@endemolshinegroup/cosmiconfig-typescript-loader": "^3.0.2",
|
||||
@ -1518,9 +1589,9 @@
|
||||
}
|
||||
},
|
||||
"@lingui/core": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-3.8.6.tgz",
|
||||
"integrity": "sha512-zuyu8tVCsctVK4A9mdlr0v1GJ1o8CpA8FE0M6vSKtz8vnPDsaPdNr9YqME6EW++w4PwhnuMyfrGx8iFIJBzp1A==",
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-3.8.9.tgz",
|
||||
"integrity": "sha512-QmEfgukR7w/4/4USZT0LGNt7Yq/RgirFl4088wEta0vgroidxaCRgUXr8RXcdFVjTdtG5dc86JTEj4inZECKvg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"make-plural": "^6.2.2",
|
||||
@ -1528,12 +1599,12 @@
|
||||
}
|
||||
},
|
||||
"@lingui/macro": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-3.8.6.tgz",
|
||||
"integrity": "sha512-WABB1ufaIB5pSoSArA0vk808OPfFCkYDzOL1foE11WS69/LEJ+1XwEd2hdFmrYppml9pLCEG+obcd2PbZXHatw==",
|
||||
"version": "3.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@lingui/macro/-/macro-3.8.9.tgz",
|
||||
"integrity": "sha512-9LhlbkJ9wOtOLhlaVRLHCRL55S5wOFyyqEhUM+ujUmCskTmMmXzjnRsw5f11nJTK1JJETMT/VlUB5/p7D7Edkw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@lingui/conf": "^3.8.6",
|
||||
"@lingui/conf": "^3.8.9",
|
||||
"ramda": "^0.27.1"
|
||||
}
|
||||
},
|
||||
@ -1885,9 +1956,9 @@
|
||||
}
|
||||
},
|
||||
"@types/codemirror": {
|
||||
"version": "0.0.108",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz",
|
||||
"integrity": "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw==",
|
||||
"version": "0.0.109",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.109.tgz",
|
||||
"integrity": "sha512-cSdiHeeLjvGn649lRTNeYrVCDOgDrtP+bDDSFDd1TF+i0jKGPDRozno2NOJ9lTniso+taiv4kiVS8dgM8Jm5lg==",
|
||||
"requires": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
@ -2013,12 +2084,12 @@
|
||||
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz",
|
||||
"integrity": "sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz",
|
||||
"integrity": "sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA==",
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.21.0",
|
||||
"@typescript-eslint/scope-manager": "4.21.0",
|
||||
"@typescript-eslint/experimental-utils": "4.22.0",
|
||||
"@typescript-eslint/scope-manager": "4.22.0",
|
||||
"debug": "^4.1.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
@ -2028,50 +2099,102 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz",
|
||||
"integrity": "sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz",
|
||||
"integrity": "sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg==",
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/scope-manager": "4.21.0",
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/typescript-estree": "4.21.0",
|
||||
"@typescript-eslint/scope-manager": "4.22.0",
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"@typescript-eslint/typescript-estree": "4.22.0",
|
||||
"eslint-scope": "^5.0.0",
|
||||
"eslint-utils": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.21.0.tgz",
|
||||
"integrity": "sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.0.tgz",
|
||||
"integrity": "sha512-z/bGdBJJZJN76nvAY9DkJANYgK3nlRstRRi74WHm3jjgf2I8AglrSY+6l7ogxOmn55YJ6oKZCLLy+6PW70z15Q==",
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.21.0",
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/typescript-estree": "4.21.0",
|
||||
"@typescript-eslint/scope-manager": "4.22.0",
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"@typescript-eslint/typescript-estree": "4.22.0",
|
||||
"debug": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz",
|
||||
"integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"@typescript-eslint/visitor-keys": "4.22.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz",
|
||||
"integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA=="
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz",
|
||||
"integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"@typescript-eslint/visitor-keys": "4.22.0",
|
||||
"debug": "^4.1.1",
|
||||
"globby": "^11.0.1",
|
||||
"is-glob": "^4.0.1",
|
||||
"semver": "^7.3.2",
|
||||
"tsutils": "^3.17.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz",
|
||||
"integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz",
|
||||
"integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==",
|
||||
"requires": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.1.1",
|
||||
"ignore": "^5.1.4",
|
||||
"merge2": "^1.3.0",
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz",
|
||||
"integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz",
|
||||
"integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/visitor-keys": "4.21.0"
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"@typescript-eslint/visitor-keys": "4.22.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz",
|
||||
"integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w=="
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz",
|
||||
"integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA=="
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz",
|
||||
"integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz",
|
||||
"integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/visitor-keys": "4.21.0",
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"@typescript-eslint/visitor-keys": "4.22.0",
|
||||
"debug": "^4.1.1",
|
||||
"globby": "^11.0.1",
|
||||
"is-glob": "^4.0.1",
|
||||
@ -2095,11 +2218,11 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz",
|
||||
"integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==",
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz",
|
||||
"integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.21.0",
|
||||
"@typescript-eslint/types": "4.22.0",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@ -2524,9 +2647,9 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.0.2.tgz",
|
||||
"integrity": "sha512-DR0GmFSlxcFJp/w//ZmbxSduAkH/AqwxoiZxK97KHnWZf6gvsKWS3160WvNMMHYvzW9OXqGWjPjVh1Qu+xDabg=="
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.1.1.tgz",
|
||||
"integrity": "sha512-ghNJersc9VD9MECwa5bL8gqvCkndW6RSCicdEHL9lIriNtXwKawlSmwo+u6KNXLYT2+f24GdFPBoynKW3ke4MQ=="
|
||||
},
|
||||
"chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
@ -2798,9 +2921,9 @@
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.20.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.20.0.tgz",
|
||||
"integrity": "sha512-nmA7y6aDH5+fknfJ0G77HQzUSfTPpq4ifq+c9blP9d+X9zs3kNjxC+t3pcbBMGTp262a6PJB3RVjLlxIgoMI+Q=="
|
||||
"version": "2.20.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.20.1.tgz",
|
||||
"integrity": "sha512-8P5M8Kxbnovd0zfvOs7ipkiVJ3/zZQ0F/nrBW4x5E+I0uAZVZ80h6CKd24fSXQ5TLK5hXMtI4yb2O5rEZdUt2A=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
@ -2962,9 +3085,9 @@
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"eslint": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz",
|
||||
"integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==",
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz",
|
||||
"integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
"@eslint/eslintrc": "^0.4.0",
|
||||
@ -3532,9 +3655,9 @@
|
||||
"integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs="
|
||||
},
|
||||
"globals": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.7.0.tgz",
|
||||
"integrity": "sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA==",
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz",
|
||||
"integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==",
|
||||
"requires": {
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
@ -5245,9 +5368,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.44.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.44.0.tgz",
|
||||
"integrity": "sha512-rGSF4pLwvuaH/x4nAS+zP6UNn5YUDWf/TeEU5IoXSZKBbKRNTCI3qMnYXKZgrC0D2KzS2baiOZt1OlqhMu5rnQ==",
|
||||
"version": "2.45.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.45.2.tgz",
|
||||
"integrity": "sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ==",
|
||||
"requires": {
|
||||
"fsevents": "~2.3.1"
|
||||
}
|
||||
@ -5764,9 +5887,9 @@
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.0.8.tgz",
|
||||
"integrity": "sha512-OBAdezyozae8IvjHGXBDHByVkLCcsmffXUSj8LXkNb0SluRd4ug3GFCjk6JynZONIPhOkyr0Nnvbq1rlIspXyQ==",
|
||||
"version": "6.0.9",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz",
|
||||
"integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==",
|
||||
"requires": {
|
||||
"ajv": "^8.0.1",
|
||||
"is-boolean-object": "^1.1.0",
|
||||
@ -5780,9 +5903,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.0.1.tgz",
|
||||
"integrity": "sha512-46ZA4TalFcLLqX1dEU3dhdY38wAtDydJ4e7QQTVekLUTzXkb1LfqU6VOBXC/a9wiv4T094WURqJH6ZitF92Kqw==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz",
|
||||
"integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
|
@ -41,9 +41,9 @@
|
||||
"@babel/preset-env": "^7.13.15",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@lingui/cli": "^3.8.6",
|
||||
"@lingui/core": "^3.8.6",
|
||||
"@lingui/macro": "^3.8.6",
|
||||
"@lingui/cli": "^3.8.9",
|
||||
"@lingui/core": "^3.8.9",
|
||||
"@lingui/macro": "^3.8.9",
|
||||
"@patternfly/patternfly": "^4.96.2",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
@ -53,18 +53,18 @@
|
||||
"@sentry/browser": "^6.2.5",
|
||||
"@sentry/tracing": "^6.2.5",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"@types/codemirror": "0.0.109",
|
||||
"@types/grecaptcha": "^3.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.21.0",
|
||||
"@typescript-eslint/parser": "^4.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.0.2",
|
||||
"chart.js": "^3.1.1",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.60.0",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-lit": "^1.3.0",
|
||||
"flowchart.js": "^1.15.0",
|
||||
@ -72,7 +72,7 @@
|
||||
"lit-html": "^1.3.0",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.44.0",
|
||||
"rollup": "^2.45.2",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
|
@ -15,8 +15,10 @@ const extensions = [
|
||||
const resources = [
|
||||
{ src: "node_modules/rapidoc/dist/rapidoc-min.js", dest: "dist/" },
|
||||
|
||||
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css.map", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/patternfly-base.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/components/Page/page.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/components/EmptyState/empty-state.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/components/Spinner/spinner.css", dest: "dist/" },
|
||||
{ src: "src/authentik.css", dest: "dist/" },
|
||||
|
||||
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
|
||||
|
@ -1,8 +1,4 @@
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { VERSION } from "../constants";
|
||||
import { SentryIgnoredError } from "../common/errors";
|
||||
import { Config, Configuration, Middleware, ResponseContext, RootApi } from "authentik-api";
|
||||
import { Configuration, Middleware, ResponseContext } from "authentik-api";
|
||||
import { getCookie } from "../utils";
|
||||
import { API_DRAWER_MIDDLEWARE } from "../elements/notifications/APIDrawer";
|
||||
import { MessageMiddleware } from "../elements/messages/Middleware";
|
||||
@ -13,13 +9,13 @@ export class LoggingMiddleware implements Middleware {
|
||||
console.debug(`authentik/api: ${context.response.status} ${context.init.method} ${context.url}`);
|
||||
return Promise.resolve(context.response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG = new Configuration({
|
||||
basePath: "/api/v2beta",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("authentik_csrf"),
|
||||
"X-Authentik-Prevent-Basic": "true"
|
||||
},
|
||||
middleware: [
|
||||
API_DRAWER_MIDDLEWARE,
|
||||
@ -27,27 +23,3 @@ export const DEFAULT_CONFIG = new Configuration({
|
||||
new LoggingMiddleware(),
|
||||
],
|
||||
});
|
||||
|
||||
export function configureSentry(): Promise<Config> {
|
||||
return new RootApi(DEFAULT_CONFIG).rootConfigList().then((config) => {
|
||||
if (config.errorReportingEnabled) {
|
||||
Sentry.init({
|
||||
dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
release: `authentik@${VERSION}`,
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing(),
|
||||
],
|
||||
tracesSampleRate: 0.6,
|
||||
environment: config.errorReportingEnvironment,
|
||||
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
|
||||
if (hint.originalException instanceof SentryIgnoredError) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
console.debug("authentik/config: Sentry enabled.");
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
50
web/src/api/Sentry.ts
Normal file
50
web/src/api/Sentry.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { VERSION } from "../constants";
|
||||
import { SentryIgnoredError } from "../common/errors";
|
||||
import { Config, RootApi } from "authentik-api";
|
||||
import { me } from "./Users";
|
||||
import { DEFAULT_CONFIG } from "./Config";
|
||||
|
||||
export function configureSentry(): Promise<Config> {
|
||||
return new RootApi(DEFAULT_CONFIG).rootConfigList().then((config) => {
|
||||
if (config.errorReportingEnabled) {
|
||||
Sentry.init({
|
||||
dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
release: `authentik@${VERSION}`,
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing({
|
||||
tracingOrigins: [window.location.host, "localhost"],
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 0.6,
|
||||
environment: config.errorReportingEnvironment,
|
||||
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
|
||||
if (hint.originalException instanceof SentryIgnoredError) {
|
||||
return null;
|
||||
}
|
||||
if (event.exception) {
|
||||
me().then(user => {
|
||||
Sentry.showReportDialog({
|
||||
eventId: event.event_id,
|
||||
user: {
|
||||
email: user.user.email,
|
||||
name: user.user.name,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
console.debug("authentik/config: Sentry enabled.");
|
||||
if (config.errorReportingSendPii) {
|
||||
me().then(user => {
|
||||
Sentry.setUser({ email: user.user.email });
|
||||
console.debug("authentik/config: Sentry with PII enabled.");
|
||||
});
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
:root {
|
||||
--ak-accent: #fd4b2d;
|
||||
|
||||
--ak-dark-foreground: #fafafa;
|
||||
--ak-dark-foreground-darker: #bebebe;
|
||||
--ak-dark-foreground-link: #5a5cb9;
|
||||
--ak-dark-background: #18191a;
|
||||
--ak-dark-background-darker: #000000;
|
||||
--ak-dark-background-light: #1c1e21;
|
||||
--ak-dark-background-lighter: #2b2e33;
|
||||
}
|
||||
|
||||
html {
|
||||
@ -89,17 +97,12 @@ html > form > input {
|
||||
body {
|
||||
background-color: var(--ak-dark-background) !important;
|
||||
}
|
||||
.ak-initial-load h1 {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--ak-dark-foreground: #fafafa;
|
||||
--ak-dark-foreground-darker: #bebebe;
|
||||
--ak-dark-foreground-link: #5a5cb9;
|
||||
--ak-dark-background: #18191a;
|
||||
--ak-dark-background-darker: #000000;
|
||||
--ak-dark-background-light: #1c1e21;
|
||||
--ak-dark-background-lighter: #2b2e33;
|
||||
|
||||
--pf-global--Color--100: var(--ak-dark-foreground);
|
||||
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
|
||||
--pf-global--link--Color: var(--ak-dark-foreground-link);
|
||||
@ -279,6 +282,7 @@ body {
|
||||
}
|
||||
.pf-c-notification-drawer__header {
|
||||
background-color: var(--ak-dark-background-lighter);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* data list */
|
||||
.pf-c-data-list__item {
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2021.4.1-rc1";
|
||||
export const VERSION = "2021.4.3";
|
||||
export const PAGE_SIZE = 20;
|
||||
export const EVENT_REFRESH = "ak-refresh";
|
||||
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
||||
|
@ -39,12 +39,20 @@ export class PageHeader extends LitElement {
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 114px;
|
||||
}
|
||||
button.sidebar-trigger {
|
||||
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
|
||||
border-radius: 0px;
|
||||
}
|
||||
.pf-c-page__main-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
img.pf-icon {
|
||||
max-height: 24px;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||
${t`On behalf of ${item.user.on_behalf_of.username}`}
|
||||
</small>` : html``}`,
|
||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||
html`<span>${item.clientIp}</span>`,
|
||||
html`<span>${item.clientIp || "-"}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`} icon="pf-icon-module">
|
||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
||||
<div slot="body">
|
||||
${t`No matching events could be found.`}
|
||||
</div>
|
||||
|
77
web/src/elements/events/UserEvents.ts
Normal file
77
web/src/elements/events/UserEvents.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { AKResponse } from "../../api/Client";
|
||||
import { Table, TableColumn } from "../table/Table";
|
||||
import { Event, EventsApi } from "authentik-api";
|
||||
|
||||
import "../forms/DeleteForm";
|
||||
import "../Tabs";
|
||||
import "../buttons/ModalButton";
|
||||
import "../buttons/SpinnerButton";
|
||||
import "../buttons/Dropdown";
|
||||
import "../../pages/events/EventInfo";
|
||||
import { PAGE_SIZE } from "../../constants";
|
||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||
import { EventWithContext } from "../../api/Events";
|
||||
|
||||
@customElement("ak-events-user")
|
||||
export class ObjectChangelog extends Table<Event> {
|
||||
expandable = true;
|
||||
|
||||
@property()
|
||||
order = "-created";
|
||||
|
||||
@property()
|
||||
targetUser!: string;
|
||||
|
||||
apiEndpoint(page: number): Promise<AKResponse<Event>> {
|
||||
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
|
||||
page: page,
|
||||
ordering: this.order,
|
||||
pageSize: PAGE_SIZE / 2,
|
||||
username: this.targetUser
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(t`Action`, "action"),
|
||||
new TableColumn(t`User`, "enabled"),
|
||||
new TableColumn(t`Creation Date`, "created"),
|
||||
new TableColumn(t`Client IP`, "client_ip"),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
return [
|
||||
html`${item.action}`,
|
||||
html`<div>${item.user?.username}</div>
|
||||
${item.user.on_behalf_of ? html`<small>
|
||||
${t`On behalf of ${item.user.on_behalf_of.username}`}
|
||||
</small>` : html``}`,
|
||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||
html`<span>${item.clientIp || "-"}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: Event): TemplateResult {
|
||||
return html`
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
||||
<div slot="body">
|
||||
${t`No matching events could be found.`}
|
||||
</div>
|
||||
</ak-empty-state>`);
|
||||
}
|
||||
|
||||
}
|
@ -35,7 +35,9 @@ export class UserOAuthCodeList extends Table<ExpiringBaseGrantModel> {
|
||||
|
||||
row(item: ExpiringBaseGrantModel): TemplateResult[] {
|
||||
return [
|
||||
html`${item.provider.name}`,
|
||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
||||
${item.provider?.name}
|
||||
</a>`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`${item.scope.join(", ")}`,
|
||||
html`
|
||||
|
@ -35,7 +35,9 @@ export class UserOAuthRefreshList extends Table<ExpiringBaseGrantModel> {
|
||||
|
||||
row(item: ExpiringBaseGrantModel): TemplateResult[] {
|
||||
return [
|
||||
html`${item.provider.name}`,
|
||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
||||
${item.provider?.name}
|
||||
</a>`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`${item.scope.join(", ")}`,
|
||||
html`
|
||||
|
@ -33,7 +33,12 @@ export class RouterOutlet extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
height: 100vh;
|
||||
background-color: var(--ak-dark-background, var(--pf-c-page--BackgroundColor)) !important;
|
||||
background-color: var(--pf-global--BackgroundColor--light-300) !important;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
background-color: var(--ak-dark-background) !important;
|
||||
}
|
||||
}
|
||||
*:first-child {
|
||||
height: 100%;
|
||||
|
@ -103,6 +103,7 @@ export class Sidebar extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
z-index: 100;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.pf-c-nav__link.pf-m-current::after,
|
||||
.pf-c-nav__link.pf-m-current:hover::after,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||
import { configureSentry } from "../../api/Config";
|
||||
import { configureSentry } from "../../api/Sentry";
|
||||
import { Config } from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
|
@ -162,11 +162,11 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
<li class="pf-c-login__main-footer-links-item">
|
||||
<a class="pf-c-button pf-m-primary pf-m-block" href="/">
|
||||
${t`Return`}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-login__main-footer-links-item">
|
||||
<a class="pf-c-button pf-m-primary pf-m-block" href="/">
|
||||
${t`Return`}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>`
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ import { css, CSSResult, html, LitElement, property, TemplateResult } from "lit-
|
||||
import { SidebarItem } from "../elements/sidebar/Sidebar";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFSkipToContent from "@patternfly/patternfly/components/SkipToContent/skip-to-content.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||
|
||||
@ -13,7 +12,9 @@ import "../elements/Banner";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import { me } from "../api/Users";
|
||||
import { t } from "@lingui/macro";
|
||||
import { EVENT_NOTIFICATION_TOGGLE, EVENT_SIDEBAR_TOGGLE } from "../constants";
|
||||
import { EVENT_NOTIFICATION_TOGGLE, EVENT_SIDEBAR_TOGGLE, VERSION } from "../constants";
|
||||
import { AdminApi } from "authentik-api";
|
||||
import { DEFAULT_CONFIG } from "../api/Config";
|
||||
|
||||
export abstract class Interface extends LitElement {
|
||||
@property({type: Boolean})
|
||||
@ -25,7 +26,7 @@ export abstract class Interface extends LitElement {
|
||||
abstract get sidebar(): SidebarItem[];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFPage, PFSkipToContent, PFButton, PFDrawer, css`
|
||||
return [PFBase, PFPage, PFButton, PFDrawer, css`
|
||||
.pf-c-page__main, .pf-c-drawer__content, .pf-c-page__drawer {
|
||||
z-index: auto !important;
|
||||
}
|
||||
@ -48,6 +49,17 @@ export abstract class Interface extends LitElement {
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
${until(new AdminApi(DEFAULT_CONFIG).adminVersionList().then(version => {
|
||||
if (version.versionCurrent !== VERSION) {
|
||||
return html`<ak-banner>
|
||||
${t`A newer version of the frontend is available.`}
|
||||
<button @click=${() => { window.location.reload(); }}>
|
||||
${t`Reload`}
|
||||
</button>
|
||||
</ak-banner>`;
|
||||
}
|
||||
return html``;
|
||||
}))}
|
||||
${until(me().then((u) => {
|
||||
if (u.original) {
|
||||
return html`<ak-banner>
|
||||
|
@ -5,16 +5,20 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="icon" type="image/png" href="/static/dist/assets/icons/icon.png">
|
||||
<link rel="shortcut icon" type="image/png" href="/static/dist/assets/icons/icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
|
||||
<script src="/static/dist/poly.js" type="module"></script>
|
||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||
<script src="/static/dist/AdminInterface.js" type="module"></script>
|
||||
<title>authentik</title>
|
||||
</head>
|
||||
<body>
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<section class="ak-initial-load pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="Loading...">
|
||||
|
@ -5,16 +5,20 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="icon" type="image/png" href="/static/dist/assets/icons/icon.png">
|
||||
<link rel="shortcut icon" type="image/png" href="/static/dist/assets/icons/icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
|
||||
<script src="/static/dist/poly.js" type="module"></script>
|
||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||
<script src="/static/dist/FlowInterface.js" type="module"></script>
|
||||
<title>authentik</title>
|
||||
</head>
|
||||
<body>
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<section class="ak-initial-load pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="Loading...">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user